RESTful 简介以及基于 SSM 的完整案例

什么是 RESTful

对于 RESTful(Representational State Transfer 表征状态转移),很多教科书把它讲得很玄乎。但其实我们可以把 RESTful 看作是 Web 开发中的一套 “行为准则” 或 “普通话标准”。它的本质就是:用最标准的 HTTP 动作,去操作服务器上的 “资源”。

核心铁三角:资源、路径、动作

理解 RESTful 只需要盯住这三个词:Resource、URI、Method

① 资源 (Resource) —— “有什么?”

在 REST 的世界里,一切皆资源。无论是你的 User 对象、一张图片、一个订单,还是 context 域里的字典项,都被视为离散的资源。资源是以名词为核心来组织的。

② 路径 (URI) —— “在哪找?”

每个资源在 Web 上都应该有一个唯一的 “身份证号”,即 URI。在使用 URI 给资源 “贴标签” 的时候,原则就是:路径里都应该是名词而不该出现动词。

  • 错误:”/getUser?id=1” 或 “/deleteUser?id=1”
  • 正确:”/users/1”

③ 动作 (Method) —— “怎么做?”

这就是状态转移(State Transfer)的体现。我们不通过 URL 里的动词来告诉服务器做什么,而是利用 HTTP 协议自带的 4 个原生动作:

所以,忘掉那些乱七八糟的动作接口,把服务器看成一个透明的资源库。 我们通过唯一的地址找到资源,选择合适的包装格式,通过交换这些包装,优雅地告诉服务器我们想让这些资源变成什么样。


基于 SSM xml 式案例演示

依赖配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<packaging>war</packaging>

<dependencies>
<!--springmvc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.2.15</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.5.15</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.15</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.15</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.27</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.5.0</version>
</dependency>

<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.19.4</version>
</dependency>

<!--数据校验标准实现-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.3.Final</version>
</dependency>

<!--测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.2.15</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<finalName>my-app-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>


主要配置文件

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">

<!--编码过滤器-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!--REST相关请求方法的支持-->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- DispatcherServlet & spring-mvc.xml -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<multipart-config>
<location></location><!--临时文件存放路径(默认"")-->
<max-file-size>2097152</max-file-size>
<max-request-size>4194304</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- "/" 表示除了 *.jsp 的请求,其他所有请求都可以匹配 -->
<url-pattern>/</url-pattern>
</servlet-mapping>

<!-- applicationContext.xml -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>

applicationContext.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--包扫描-->
<context:component-scan base-package="com.demo">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!--外部参数-->
<context:property-placeholder location="classpath:jdbc.properties"/>

<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="maxWait" value="${jdbc.maxWait}"/>
<property name="socketTimeout" value="${jdbc.socketTimeout}"/>
</bean>

<!--事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

<!--mybatis plus-->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
<property name="typeAliasesPackage" value="com.demo.entity"/>
<property name="plugins">
<array>
<bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors">
<list>
<bean class="com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor">
<property name="dbType" value="MYSQL"/>
<property name="maxLimit" value="500"/>
</bean>
</list>
</property>
</bean>
</array>
</property>
<property name="configuration">
<bean class="com.baomidou.mybatisplus.core.MybatisConfiguration">
<property name="defaultStatementTimeout" value="30"/>
<property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/>
</bean>
</property>
<property name="globalConfig" ref="globalConfig"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.demo.mapper"/>
</bean>
<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
<property name="metaObjectHandler">
<bean class="com.demo.config.MybatisPlusMetaObjectHandler"/>
</property>
<property name="dbConfig">
<bean class="com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig">
<property name="logicDeleteValue" value="1"/> <!--定义逻辑删除标记-->
<property name="logicNotDeleteValue" value="0"/>
</bean>
</property>
</bean>
</beans>

jdbc.properties:

1
2
3
4
5
6
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://xxx.xxx:3306/demo?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
jdbc.username=xxx
jdbc.password=xxx
jdbc.maxWait=5000
jdbc.socketTimeout=3000

spring-mvc.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?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:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!--包扫描-->
<context:component-scan base-package="com.demo.controller"/>

<!--核心作用是注册了两个支撑 Spring MVC 运行的灵魂组件:RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter-->
<!--具体包括:数据绑定与类型转换、JSON 自动转换、数据校验、路径匹配支持Ant风格等优化-->
<!--等价于注解开发中的 @Configuration @EnableWebMvc-->
<mvc:annotation-driven />

<!--Thymeleaf 视图解析器-->
<bean id="templateResolver" class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8" />
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML" />
</bean>
<bean id="templateEngine" class="org.thymeleaf.spring6.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
<bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<property name="characterEncoding" value="UTF-8" />
</bean>

<!--JSP 视图解析器-->
<!--<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/templates/"/>
<property name="suffix" value=".jsp"/>
</bean>-->

<!--静态资源映射-->
<!--注:如果配置了 <mvc:default-servlet-handler/>(把请求转发给Tomcat默认的Servlet),那么 throwExceptionIfNoHandlerFound 将失效-->
<!--所以这里改用 <mvc:resources> 来接管静态资源-->
<mvc:resources mapping="/static/**" location="/static/" />

<!--文件上传支持-->
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>

<!--拦截器链(洋葱模型)-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="com.demo.interceptor.ExecutionTimeInterceptor"/>
</mvc:interceptor>

<mvc:interceptor>
<mvc:mapping path="/emps/**"/>
<bean class="com.demo.interceptor.SecurityHeaderInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
</beans>


MybatisPlus 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author KJ
* @description mybatis plus 元对象字段填充
*/
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 插入时,填充 createTime 和 updateTime 为当前时间
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
// 填充 deleted 字段
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
}

@Override
public void updateFill(MetaObject metaObject) {
// strictUpdateFill 默认遵循 “如果不为空则不填充” 的原则
// this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
// 强制填充(用于更新时间),不管对象里有没有值都覆盖
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}


基础数据模型

基类 BaseEntity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

@Data
public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = -3449227427576415651L;

private String createBy;
private String updateBy;

/** 创建时间,插入时填充 */
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/** 更新时间,插入和更新时填充 */
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;

/** 逻辑删除,0-正常,1-删除 */
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;

/** 分页和查询参数 */
@TableField(exist = false)
private Integer pageNum = 1;
@TableField(exist = false)
private Integer pageSize = 10;

@TableField(exist = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@TableField(exist = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
}

响应码 ResultCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public enum ResultCode {

/* 成功 */
SUCCESS(0, "操作成功"),
/* 失败 */
ERROR(500, "服务异常"),
;

private final int code;
private final String message;

ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
}

视图层统一VO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Data
@Accessors(chain = true)
public class R<T> implements Serializable {
private static final long serialVersionUID = -1837650589620080609L;

private int code;
private String msg;
private T data;
private long timestamp;

public static <T> R<T> ok() {
return of(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
}

public static <T> R<T> ok(T data) {
return of(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}

public static <T> R<T> ok(T data, String msg) {
return of(ResultCode.SUCCESS.getCode(), msg, data);
}

public static <T> R<T> fail() {
return of(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMessage(), null);
}

public static <T> R<T> fail(String msg) {
return of(ResultCode.ERROR.getCode(), msg, null);
}

public static <T> R<T> fail(T data) {
return of(ResultCode.ERROR.getCode(), "操作失败", data);
}

public static <T> R<T> fail(T data, String msg) {
return of(ResultCode.ERROR.getCode(), msg, data);
}

public static <T> R<T> fail(int code, String msg) {
return of(code, msg, null);
}

private static <T> R<T> of(int code, String msg, T data) {
R<T> result = new R<>();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}


业务类和方法

Emp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_emp")
public class Emp extends BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;

@NotBlank(message = "员工姓名不能为空")
@Size(min = 2, max = 10, message = "姓名长度需在2-10个字符之间")
private String name;

@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 65, message = "年龄不能大于65岁")
private Integer age;

@NotNull(message = "所属部门不能为空")
private Long deptId;

// 关联查询字段(不属于数据库表)
@TableField(exist = false)
private Dept dept;
}

Dept:

1
2
3
4
5
6
7
8
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_dept")
public class Dept extends BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
}

EmpMapper:

1
2
3
4
5
6
7
@Mapper
public interface EmpMapper extends BaseMapper<Emp> {

IPage<Emp> selectPage(Page<Emp> page, @Param("q") Emp emp); // 注意如果是分页,page放第一个入参

Emp selectById(@Param("id") Long id);
}

EmpMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo.mapper.EmpMapper">

<resultMap id="EmpMap" type="com.demo.entity.Emp">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<result property="deptId" column="dept_id"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<association property="dept" javaType="com.demo.entity.Dept">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
</association>
</resultMap>

<sql id="BASE_COLUMN">
e.id, e.name, e.age, e.dept_id, e.create_time, e.update_time, d.name as dept_name
</sql>

<select id="selectPage" resultMap="EmpMap">
SELECT <include refid="BASE_COLUMN"/>
FROM t_emp e LEFT JOIN t_dept d ON e.dept_id = d.id
<where>
AND e.deleted = 0
<if test="q.name != null and q.name != ''">
AND e.name LIKE CONCAT('%', #{q.name}, '%')
</if>
<if test="q.deptId != null">
AND e.dept_id = #{q.deptId}
</if>
<if test="q.startTime != null">
AND e.create_time &gt;= #{q.startTime}
</if>
<if test="q.endTime != null">
AND e.create_time &lt;= #{q.endTime}
</if>
</where>
</select>

<select id="selectById" resultMap="EmpMap">
SELECT <include refid="BASE_COLUMN"/>
FROM t_emp e LEFT JOIN t_dept d ON e.dept_id = d.id
WHERE e.id = #{id} AND e.deleted = 0
</select>
</mapper>

EmpService:

1
2
3
4
5
6
7
public interface EmpService {
IPage<Emp> queryPage(Page<Emp> page, Emp emp);
Emp getById(Long id);
void add(Emp emp);
void modify(Emp emp);
boolean remove(Long id);
}

EmpServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {
@Override
public IPage<Emp> queryPage(Page<Emp> page, Emp emp) {
return baseMapper.selectPage(page, emp);
}

@Override
public Emp getById(Long id) {
return baseMapper.selectById(id);
}

@Transactional(rollbackFor = Exception.class)
@Override
public void add(Emp emp) {
this.save(emp);
}

@Transactional(rollbackFor = Exception.class)
@Override
public void modify(Emp emp) {
this.updateById(emp);
}

@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(Long id) {
return this.removeById(id); // 到底是逻辑删除还是物理删除,取决于你是否配置了 @TableLogic
}
}

EmpController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.demo.entity.Emp;
import com.demo.service.EmpService;
import com.demo.vo.R;
import jakarta.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
* @author KJ
* @description 员工管理控制器
*/
@Controller
@RequestMapping("/emps")
public class EmpController {

@Autowired
private EmpService empService;

@GetMapping
public String index() {
return "emp";
}

/**
* 分页(GET)
*/
@ResponseBody
@GetMapping("/page")
public R<IPage<Emp>> queryPage(Emp emp) {
Page<Emp> page = new Page<>(emp.getPageNum(), emp.getPageSize());
page.addOrder(OrderItem.desc("create_time"));

IPage<Emp> resultPage = empService.queryPage(page, emp);
return R.ok(resultPage);
}

/**
* 详情 (GET)
*/
@ResponseBody
@GetMapping("/{id}")
public R<Emp> getInfo(@PathVariable Long id) {
Emp emp = empService.getById(id);
Assert.notNull(emp, "该员工不存在或已被删除");
return R.ok(emp);
}

/**
* 新增 (POST)
*/
@ResponseBody
@PostMapping
public R<Void> add(@RequestBody @Validated Emp emp) {
Assert.hasText(emp.getName(), "员工姓名不能为空");
empService.add(emp);
return R.ok();
}

/**
* 修改 (PUT)
*/
@ResponseBody
@PutMapping("/{id}")
public R<Void> update(@PathVariable Long id, @RequestBody @Validated Emp emp) {
emp.setId(id);
empService.modify(emp);
return R.ok();
}

/**
* 删除 (DELETE)
*/
@ResponseBody
@DeleteMapping("/{id}")
public R<Void> delete(@PathVariable @NotNull(message = "ID不能为空") Long id) {
boolean removed = empService.remove(id);
return removed ? R.ok() : R.fail();
}
}


异常捕获处理

GlobalExceptionHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Slf4j
@ControllerAdvice // 拦截所有 Controller 的异常
public class GlobalExceptionHandler {

/**
* 处理 404 页面跳转
* 场景:用户在浏览器地址栏乱输 URL
*/
@ExceptionHandler(NoHandlerFoundException.class)
public String handle404() {
return "404";
}

/**
* 处理校验异常 (JSON 响应)
* 场景:Axios 提交表单数据不合法
*/
@ResponseBody // 返回的是 JSON 而不是视图
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError().getDefaultMessage(); // 获取第一条校验失败的消息
return R.fail(message);
}

@ResponseBody
@ExceptionHandler(ConstraintViolationException.class)
public R<Void> handleConstraintViolationException(ConstraintViolationException e) {
return R.fail(e.getMessage());
}

/**
* 3. 混合处理运行时异常
* 核心逻辑:判断是 Ajax 请求还是普通页面请求
*/
@ExceptionHandler(RuntimeException.class)
public Object handleRuntimeException(HttpServletRequest request, HttpServletResponse response, RuntimeException e) throws IOException {
log.error("系统运行异常: ", e);

// 判断请求头,如果是 Ajax/Axios 请求,返回 R 对象 (JSON)
String header = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(header)) {
response.setContentType("application/json;charset=UTF-8");
R<String> result = R.fail(ResultCode.ERROR.getCode(), e.getMessage());
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(result));
return null; // 返回 null 告诉 Spring 我们已经手动处理完响应了
}

// 如果是普通浏览器请求,返回错误页面 (Thymeleaf)
ModelAndView mav = new ModelAndView("error");
mav.addObject("ex", e.getMessage());
return mav;
}

@ResponseBody
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
log.error("系统未知错误: ", e);
return R.fail(ResultCode.ERROR.getCode(), "服务器冒烟了,请稍后再试");
}
}


拦截器支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author KJ
* @description 性能监控拦截器
*/
public class ExecutionTimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 记录开始时间
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.println(request.getRequestURI() + " 接口耗时: " + (endTime - startTime) + "ms");
}
}

/**
* 设置响应头放在 preHandle 而不是 postHandle 的理由:
* ① 如果你在 postHandle 里注入安全头(如 X-Frame-Options 或自定义的 X-Powered-By),一旦程序报错,返回给前端的错误响应里将丢失这些头信息。
* ② 如果响应是一个大文件下载流(StreamingResponseBody)或已经手动执行了 response.flushBuffer(),当程序运行到 postHandle 时,响应头可能已经发送给浏览器了。此时再调用 setHeader 可能无效或报错。
*
* @author KJ
* @description Header 注入拦截器 (在响应中加入特定头)
*/
public class SecurityHeaderInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 在响应中加入特定的版本或安全头,前端可以根据此头进行校验
response.setHeader("X-Powered-By", "Owlias");
response.setHeader("X-App-Version", "Version1.0");
return true;
}
}


Thymeleaf+Vue3 视图

emp.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工管理</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<style>
.el-main { background-color: #f5f7fa; min-height: 100vh; padding: 20px; }
.search-card { margin-bottom: 20px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
</style>
</head>
<body>
<div id="app">
<el-container>
<el-main>
<el-card class="search-card" shadow="never">
<el-form :inline="true" :model="searchForm">
<el-form-item label="姓名">
<el-input v-model="searchForm.name" placeholder="模糊查询" clearable></el-input>
</el-form-item>
<el-form-item label="部门">
<el-select v-model="searchForm.deptId" placeholder="选择部门" clearable>
<el-option label="研发部" :value="1"></el-option>
<el-option label="测试部" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始"
end-placeholder="结束"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateChange">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>

<div class="page-header">
<h3>员工列表</h3>
<el-button type="primary" @click="openDialog()">新增员工</el-button>
</div>

<el-table :data="tableData" border v-loading="loading">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="age" label="年龄" width="70"></el-table-column>
<el-table-column label="部门">
<template #default="scope">
<el-tag v-if="scope.row.dept">{{ scope.row.dept.name }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="入职时间" width="180">
<template #default="scope">
{{ scope.row.createTime || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="openDialog(scope.row.id)">详情</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>

<el-pagination
style="margin-top: 20px; justify-content: flex-end;"
v-model:current-page="searchForm.pageNum"
v-model:page-size="searchForm.pageSize"
:total="total"
:page-sizes="[5, 10, 20]"
layout="total, sizes, prev, pager, next"
@size-change="fetchData"
@current-change="fetchData">
</el-pagination>

<el-dialog v-model="dialogVisible" :title="form.id ? '编辑员工' : '新增员工'" width="400px">
<el-form :model="form" label-width="80px">
<el-form-item label="姓名">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="年龄">
<el-input-number v-model="form.age" :min="18"></el-input-number>
</el-form-item>
<el-form-item label="部门">
<el-select v-model="form.deptId" style="width: 100%">
<el-option label="研发部" :value="1"></el-option>
<el-option label="测试部" :value="2"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEmp">保存</el-button>
</template>
</el-dialog>
</el-main>
</el-container>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/element-plus"></script>

<script>
const { createApp, ref, reactive, onMounted } = Vue;

createApp({
setup() {
const loading = ref(false);
const tableData = ref([]);
const total = ref(0);
const dialogVisible = ref(false);
const dateRange = ref([]);

// 搜索参数 (对应后端的 Emp 对象)
const searchForm = reactive({
pageNum: 1,
pageSize: 10,
name: '',
deptId: null,
startTime: null,
endTime: null
});

// 实体表单
const form = reactive({
id: null, name: '', age: 20, deptId: null
});

// 获取数据
const fetchData = async () => {
loading.value = true;
try {
// 利用 Axios 请求你的 Spring MVC Controller 接口
const res = await axios.get('/emps/page', { params: searchForm });
if (res.data.code === 0) {
tableData.value = res.data.data.records;
total.value = res.data.data.total;
} else {
console.error('业务报错:', res.data.msg);
}
} catch (error) {
console.error('网络请求失败:', error);
} finally {
loading.value = false;
}
};

const handleDateChange = (val) => {
searchForm.startTime = val ? val[0] : null;
searchForm.endTime = val ? val[1] : null;
};

const resetSearch = () => {
Object.assign(searchForm, { pageNum: 1, name: '', deptId: null, startTime: null, endTime: null });
dateRange.value = [];
fetchData();
};

// 打开弹窗(详情/新增)
const openDialog = async (id = null) => {
if (id) {
const res = await axios.get(`/emps/${id}`);
Object.assign(form, res.data.data);
} else {
Object.assign(form, { id: null, name: '', age: 20, deptId: null });
}
dialogVisible.value = true;
};

// 保存(POST/PUT)
const saveEmp = async () => {
try {
const method = form.id ? 'put' : 'post';
const url = form.id ? `/emps/${form.id}` : '/emps';

// 1. 发起请求
const res = await axios[method](url, form);

// 2. 业务状态码判断 (根据你之前的 JSON 结果,成功是 0)
if (res.data.code === 0) {
ElementPlus.ElMessage.success('操作成功');
dialogVisible.value = false;
fetchData(); // 刷新列表
} else {
// 3. 处理后端验证失败等业务异常 (如:姓名重复、年龄不合规)
ElementPlus.ElMessage.error(res.data.msg || '保存失败');
}
} catch (error) {
// 4. 处理网络、权限或服务器崩溃等系统异常
console.error('Save Error:', error);
ElementPlus.ElMessage.error('系统繁忙,请稍后再试');
}
};

// 删除 (DELETE)
const handleDelete = (id) => {
ElementPlus.ElMessageBox.confirm('确定逻辑删除该员工吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await axios.delete(`/emps/${id}`);

if (res.data.code === 0) {
ElementPlus.ElMessage.success('删除成功');
fetchData();
} else {
ElementPlus.ElMessage.error(res.data.msg || '删除失败');
}
} catch (error) {
ElementPlus.ElMessage.error('网络请求异常');
}
}).catch(() => {
// 用户取消删除,无需处理
});
};

onMounted(fetchData);

return {
tableData, total, searchForm, form, loading, dialogVisible, dateRange,
fetchData, resetSearch, handleDateChange, openDialog, saveEmp, handleDelete
};
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>


测试单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @author KJ
*/
@SpringJUnitConfig(locations = {"classpath:applicationContext.xml"})
public class EmpTest {

@Autowired
private EmpService empService;

@Test
public void test01() {
Emp emp = empService.getById(1L);
System.out.println(emp);
}

@Test
public void test02() {
Page<Emp> page = new Page<>(1, 10);
page.addOrder(OrderItem.desc("create_time"));

Emp q = new Emp();
q.setStartTime(new Date());
q.setEndTime(new Date());

IPage<Emp> pageResult = empService.queryPage(page, q);
System.out.println(pageResult);
}

@Test
public void test03() {
Emp emp = new Emp();
emp.setName("wangwu");
emp.setAge(24);
empService.add(emp);
}

@Test
public void test04() {
Emp emp = new Emp();
emp.setId(16L);
emp.setName("wangwu2");
emp.setAge(26);
empService.modify(emp);
}

@Test
public void test05() {
boolean result = empService.remove(16L);
System.out.println(result);
}
}


展示效果


上述案例改为全注解式

去掉 web.xml

web.xml => WebInit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import jakarta.servlet.Filter;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
* 全注解开发,告别 web.xml 的核心启动类
*/
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

// 指定父容器配置类 (替代 applicationContext.xml)
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { RootConfig.class };
}

// 指定子容器配置类 (替代 spring-mvc.xml)
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebMvcConfig.class };
}

// 注册过滤器 (替代 web.xml 中的 Filter)
@Override
protected Filter[] getServletFilters() {
// 1. 创建编码过滤器
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceResponseEncoding(true);

// 2. 创建 REST 请求方法支持过滤器
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();

// 返回数组的顺序即为 Filter 执行的顺序(必须把 encodingFilter 放在第一位,确保请求在被处理前先设置编码)
return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
}

// 指定 DispatcherServlet 的映射路径
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}

// 5. 其他自定义注册
@Override
protected void customizeRegistration(jakarta.servlet.ServletRegistration.Dynamic registration) {
// 如果没有找到映射,抛出异常而不是交给容器处理
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");

// 这里的参数等价于 Servlet 中的 @MultipartConfig
registration.setMultipartConfig(new jakarta.servlet.MultipartConfigElement("", 2097152, 4194304, 0)); // 2MB, 4MB, 阈值 0

//
super.customizeRegistration(registration);
}
}


去掉 spring-mvc.xml

spring-mvc.xml => WebMvcConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;

@Configuration
@EnableWebMvc // 开启 Spring MVC 注解驱动
@ComponentScan("com.demo.controller") // 扫描控制器
public class WebMvcConfig implements WebMvcConfigurer {

// --- Thymeleaf 视图解析器配置 ---
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setPrefix("/WEB-INF/templates/");
resolver.setSuffix(".html");
resolver.setCharacterEncoding("UTF-8");
resolver.setTemplateMode(TemplateMode.HTML); // 明确指定 HTML 模式
return resolver;
}

@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver());
return engine;
}

@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
return resolver;
}

// 文件上传支持
@Bean
public StandardServletMultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}

// 配置静态资源处理,替代 <mvc:resources />
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 1. 注册耗时监控拦截器
registry.addInterceptor(new ExecutionTimeInterceptor())
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/static/**"); // 排除静态资源

// 2. 注册安全头拦截器
registry.addInterceptor(new SecurityHeaderInterceptor())
.addPathPatterns("/emps/**"); // 只针对接口增加安全头
}
}


去掉 applicationContext.xml

applicationContext.xml => RootConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.demo.config.MybatisPlusMetaObjectHandler;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;

/**
* @author KJ
* @description
*/
@Configuration
@PropertySource("classpath:jdbc.properties")
@ComponentScan(basePackages = "com.demo", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
})
@MapperScan("com.demo.mapper")
@EnableTransactionManagement // 开启事务控制
public class RootConfig {

@Value("${jdbc.driver}") private String driver;
@Value("${jdbc.url}") private String url;
@Value("${jdbc.username}") private String username;
@Value("${jdbc.password}") private String password;
@Value("${jdbc.maxWait}") private Integer maxWait;
@Value("${jdbc.socketTimeout}") private Integer socketTimeout;

@Bean
@Primary
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setMaxWait(maxWait);
ds.setSocketTimeout(socketTimeout);
return ds;
}

@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, GlobalConfig globalConfig) throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
factoryBean.setTypeAliasesPackage("com.demo.entity");

// 分页插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
factoryBean.setPlugins(interceptor);

// MybatisConfiguration
MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
mybatisConfiguration.setDefaultStatementTimeout(30);
mybatisConfiguration.setLogImpl(StdOutImpl.class);
factoryBean.setConfiguration(mybatisConfiguration);

// GlobalConfig
factoryBean.setGlobalConfig(globalConfig);
return factoryBean;
}

@Bean
public GlobalConfig globalConfig() {
GlobalConfig config = new GlobalConfig();
config.setMetaObjectHandler(new MybatisPlusMetaObjectHandler());
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setLogicDeleteValue("1"); // 定义逻辑删除标记
dbConfig.setLogicNotDeleteValue("0");
config.setDbConfig(dbConfig);
return config;
}
}

其他均保持原样即可,全注解式开发效果和原来配置文件完全一样。