avatar


18.SSM

SSM = Spring + SpringMVC + Mybatis

整合

整合配置

1、创建Maven的web项目

关于基于Maven创建Web项目,可以参考我们在《13.Servlet、Filter和Listener》的讨论。

2、添加依赖

需要添加的依赖有:

  1. Spring相关的:
    spring-context?这个不需要的,我们引入的SpringMVC的坐标会包含spring-context,这个我们在上一章《17.SpringMVC》的"整合Spring"有过讨论。
  2. SpringMVC相关的:
    spring-webmvc
    javax.servlet-api
    jackson-databind,SpringMVC解析JSON需要用。
  3. MyBatis相关的:
    mybatis
    mybatis-spring
    spring-jdbc
  4. MySQL相关的:
    mysql-connector-java
  5. 数据库连接池:
    druid
  6. Junit相关的:
    junit
    spring-test

即,最后的pom.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
<dependencies>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.24</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.24</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.24</version>
<scope>test</scope>
</dependency>

</dependencies>

我们是基于JDK8开发的,如果我们引入的mybatis-spring为3版本的。在运行的过程中,可能会出现如下的提示:

1
2
3
4
java: cannot access org.mybatis.spring.SqlSessionFactoryBean
bad class file: /Users/kaka/.m2/repository/org/mybatis/mybatis-spring/3.0.1/mybatis-spring-3.0.1.jar!/org/mybatis/spring/SqlSessionFactoryBean.class
class file has wrong version 61.0, should be 52.0
Please remove or make sure it appears in the correct subdirectory of the classpath.

3、创建项目包结构

项目包结构

解释说明:

  • config,存放配置类。
  • controller,存放Controller类。
  • dao,存放Dao接口,因为使用的是Mapper接口代理方式,所以没有实现类包。
  • service,存放Service接口。
  • service.impl,存放Service的实现类。
  • resources,存放配置文件,如jdbc.properties
  • webapp:存放静态资源。
  • test/java:存放测试类。

4、创建jdbc.properties

src/main/resources目录下创建jdbc.properties,配置数据库的连接要素。

1
2
3
jdbc.url=jdbc:mysql://127.0.0.1:3306/ssm
jdbc.username=root
jdbc.password=MySQL@2022

5、创建JdbcConfig配置类

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
package com.kakawanyifan.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

public class JdbcConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager ds = new DataSourceTransactionManager();
ds.setDataSource(dataSource);
return ds;
}
}

6、创建MybatisConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.kakawanyifan.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("com.kakawanyifan.pojo");
return factoryBean;
}

@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.kakawanyifan.dao");
return msc;
}
}
  • factoryBean.setTypeAliasesPackage("com.kakawanyifan.pojo");,类型别名的扫描包。

7、创建SpringConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kakawanyifan.config;

import org.springframework.context.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan(value = "com.kakawanyifan",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Controller.class
))
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MyBatisConfig.class})
@EnableTransactionManagement
public class SpringConfig {

}

关于为什么要在SpringConfig配置类中排除掉Controller,我们在上一章《17.SpringMVC》的"整合Spring"有过讨论。

  1. 如果我们把Bean全放在Spring中。
    当访问项目的时候,会因为SpringMVC找不到处理器映射器和其相应的controller,而报404错误。
  2. 如果我们把Bean全部放到springMVC中。
    1. 拓展性不强。
    2. 因为事务管理器是在Spring中的,这么做可能会导致事务管理器失效。

所以SpringMVC管理controller,Spring管理controller之外的。

8、创建SpringMVC配置类

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan("com.kakawanyifan.controller")
@EnableWebMvc
public class SpringMvcConfig {

}

9、创建Web项目入口配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kakawanyifan.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
//加载Spring配置类
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
//加载SpringMVC配置类
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
//设置SpringMVC请求地址拦截规则
protected String[] getServletMappings() {
return new String[]{"/"};
}
}

功能模块开发

假设现在存在一个库ssm,如下:

1
create database ssm character set utf8;

ssm库上存在一个表book,如下:

1
2
3
4
5
6
create table book(
id int primary key auto_increment,
type varchar(20),
name varchar(50),
description varchar(255)
)

表中有数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
insert into `book`(`type`, `name`, `description`)
values ('计算机理论', 'Spring实战 第五版', 'Spring入门经典教程,深入理解Spring原理技术内幕'),
('计算机理论', 'Spring 5核心原理与30个类手写实践', '十年沉淀之作,手写Spring精华思想'),
('计算机理论', 'Spring 5设计模式', '深入Spring源码刨析Spring源码中蕴含的10大设计模式'),
('计算机理论', 'Spring MVC+Mybatis开发从入门到项目实战', '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'),
('计算机理论', '轻量级Java Web企业应用实战', '源码级刨析Spring框架,适合已掌握Java基础的读者'),
('计算机理论', 'Java核心技术 卷Ⅰ 基础知识(原书第11版)', 'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新'),
('计算机理论', '深入理解Java虚拟机', '5个纬度全面刨析JVM,大厂面试知识点全覆盖'),
('计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉'),
('计算机理论', '零基础学Java(全彩版)', '零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术'),
('市场营销', '直播就这么做:主播高效沟通实战指南', '李子柒、李佳奇、薇娅成长为网红的秘密都在书中'),
('市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍'),
('市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');

POJO类

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan.pojo;

import lombok.Data;

import java.io.Serializable;

@Data
public class Book implements Serializable {
private Integer id;
private String type;
private String name;
private String description;
}

在这里,我们为了避免频繁的写Getter和Setter方法,利用了字节码增强器lombok
这样只需要在相关的POJO上标示注解@Data

lombok的坐标如下:

1
2
3
4
5
6
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>

Dao接口

示例代码:

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
package com.kakawanyifan.dao;

import com.kakawanyifan.pojo.Book;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BookDao {
@Insert("insert into book (type,name,description) values(#{type},#{name},#{description})")
public void save(Book book);

@Update("update book set type = #{type}, name = #{name}, description = #{description} where id = #{id}")
public void update(Book book);

@Delete("delete from book where id = #{id}")
public void delete(Integer id);

@Select("select * from book where id = #{id}")
public Book getById(Integer id);

@Select("select * from book")
public List<Book> getAll();
}

Service接口及其实现类

Service接口,示例代码:

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
package com.kakawanyifan.service;

import com.kakawanyifan.pojo.Book;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
public interface BookService {

/**
* 保存
* @param book
* @return
*/
public boolean save(Book book);

/**
* 修改
* @param book
* @return
*/
public boolean update(Book book);

/**
* 按id删除
* @param id
* @return
*/
public boolean delete(Integer id);

/**
* 按id查询
* @param id
* @return
*/
public Book getById(Integer id);

/**
* 查询全部
* @return
*/
public List<Book> getAll();
}

实现类,示例代码:

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
package com.kakawanyifan.service.impl;

import com.kakawanyifan.dao.BookDao;
import com.kakawanyifan.pojo.Book;
import com.kakawanyifan.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;

public boolean save(Book book) {
bookDao.save(book);
return true;
}

public boolean update(Book book) {
bookDao.update(book);
return true;
}

public boolean delete(Integer id) {
bookDao.delete(id);
return true;
}

public Book getById(Integer id) {
return bookDao.getById(id);
}

public List<Book> getAll() {
return bookDao.getAll();
}
}

我们可能会在IDEA上看到这个:
bookDao在Service中注入的会提示一个红线提示

bookDao在Service中注入的会提示一个红线提示。
这是因为IDEA不建议"基于字段变量的依赖注入"。

《15.Spring Framework [1/2]》,我们讨论过注入,有"Setter方法注入"和"构造方法注入"。
在这里,我们采用@Autowired自动装配,这种方式被称为"基于字段变量的依赖注入"。

如果想继续使用@Autowired基于字段变量的依赖注入,解决办如下:
打开IDEA的设置,依次点开EditorInspections,在右侧框内,依次点开SpringSpring CoreCoreField Injection warning,将Severity属性值改为No highlighting,only fix

@Autowired

Contorller类

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
package com.kakawanyifan.controller;

import com.kakawanyifan.pojo.Book;
import com.kakawanyifan.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {

@Autowired
private BookService bookService;

@PostMapping
public boolean save(@RequestBody Book book) {
return bookService.save(book);
}

@PutMapping
public boolean update(@RequestBody Book book) {
return bookService.update(book);
}

@DeleteMapping("/{id}")
public boolean delete(@PathVariable Integer id) {
return bookService.delete(id);
}

@GetMapping("/{id}")
public Book getById(@PathVariable Integer id) {
return bookService.getById(id);
}

@GetMapping
public List<Book> getAll() {
return bookService.getAll();
}
}

单元测试

示例代码:

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
package com.kakawanyifan.service.impl;

import com.kakawanyifan.config.SpringConfig;
import com.kakawanyifan.pojo.Book;
import com.kakawanyifan.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
@WebAppConfiguration
public class BookServiceTest {

@Autowired
private BookService bookService;

@Test
public void testGetById(){
Book book = bookService.getById(1);
System.out.println(book);
}

@Test
public void testGetAll(){
List<Book> all = bookService.getAll();
System.out.println(all);
}

}

注意,一定要有@WebAppConfiguration这个注解,否则会有如下的报错:

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

【部分运行结果略】

java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:98)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)

【部分运行结果略】

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceHandlerMapping' defined in class path resource [org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:638)

【部分运行结果略】

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)
... 43 more
Caused by: java.lang.IllegalStateException: No ServletContext set
at org.springframework.util.Assert.state(Assert.java:76)
at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.resourceHandlerMapping(WebMvcConfigurationSupport.java:591)

【部分运行结果略】

java.lang.IllegalStateException: Failed to load ApplicationContext

at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:98)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:118)

【部分运行结果略】

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceHandlerMapping' defined in class path resource [org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:638)

【部分运行结果略】

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)
... 43 more
Caused by: java.lang.IllegalStateException: No ServletContext set
at org.springframework.util.Assert.state(Assert.java:76)
at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.resourceHandlerMapping(WebMvcConfigurationSupport.java:591)

【部分运行结果略】

有些资料会建议,在进行Junit测试的时候,去除SpringMvcConfig@EnableWebMvc
这个虽然也能解决问题,但绝对不是一个好办法。

Postman测试

配置Tomcat环境

关于如何配置Tomcat环境,可以参考《13.Servlet、Filter和Listener》的"在IDEA使用Tomcat"。

利用Postman测试

利用Postman测试

XML方式

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--Spring的监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>DispatcherServlet</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>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</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
<?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"
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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--配置注解扫描-->
<context:component-scan base-package="com.kakawanyifan">
<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">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<bean id="SqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
</bean>

<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.kakawanyifan.dao"></property>
</bean>

<!--平台事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 事务增强配置 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<!-- 事务的AOP增强 -->
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(* com.kakawanyifan.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"></aop:advisor>
</aop:config>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

</beans>

spring-mvc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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:mvc="http://www.springframework.org/schema/mvc"
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/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!--配置注解扫描-->
<context:component-scan base-package="com.kakawanyifan.controller"/>

<mvc:annotation-driven/>

</beans>

jdbc.properties

1
2
3
jdbc.url=jdbc:mysql://127.0.0.1:3306/ssm
jdbc.username=root
jdbc.password=MySQL@2022

注意,需要引入包aspectjweaver

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>

结果封装

协议定义

在上文,对于不同的操作,我们会返回不同结构的报文?

表现层与前端数据传输协议定义

这里,我们考虑对结构进行统一。

创建Result类

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kakawanyifan.pojo;

import lombok.Data;

import java.io.Serializable;

@Data
public class Result implements Serializable {
private Object data;
private Integer code;
private String msg;

public Result(Object data, Integer code, String msg) {
this.data = data;
this.code = code;
this.msg = msg;
}
}

解释说明:

  • 返回的结果,封装到data属性。
  • 返回的状态码,封装在code属性。
  • 返回的状态信息,封装在msg属性。

定义Constant类

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan.constant;

public class ResultConstant {
// 成功
public static final Integer SUCCESS = 1;
public static final String SUCCESS_MSG = "成功";

// 成功无结果
public static final Integer SUCCESS_NODATA = 2;
public static final String SUCCESS_NODATA_MSG = "查询成功无结果";

// 失败
public static final Integer FAIL = 3;
public static final String FAIL_MSG = "失败";
}

修改Controller类的返回

示例代码:

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
package com.kakawanyifan.controller;

import com.kakawanyifan.constant.ResultConstant;
import com.kakawanyifan.pojo.Result;
import com.kakawanyifan.pojo.Book;
import com.kakawanyifan.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {

@Autowired
private BookService bookService;

@PostMapping
public Result save(@RequestBody Book book) {
Integer flag = bookService.save(book) ? ResultConstant.SUCCESS : ResultConstant.FAIL;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.FAIL_MSG;
return new Result(null, flag,flagMsg);
}

@PutMapping
public Result update(@RequestBody Book book) {
Integer flag = bookService.update(book) ? ResultConstant.SUCCESS : ResultConstant.FAIL;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.FAIL_MSG;
return new Result(null, flag,flagMsg);
}

@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
Integer flag = bookService.delete(id) ? ResultConstant.SUCCESS : ResultConstant.FAIL;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.FAIL_MSG;
return new Result(null,flag,flagMsg);
}

@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Book book = bookService.getById(id);
Integer flag = book != null ? ResultConstant.SUCCESS : ResultConstant.SUCCESS_NODATA;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.SUCCESS_NODATA_MSG;
return new Result(book,flag,flagMsg);
}

@GetMapping
public Result getAll() {
List<Book> bookList = bookService.getAll();
Integer flag = (bookList != null && bookList.size() > 0) ? ResultConstant.SUCCESS : ResultConstant.SUCCESS_NODATA;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.SUCCESS_NODATA_MSG;
return new Result(bookList,flag,flagMsg);
}

}

ResponseBodyAdvice

在上文,我们需要一个一个写,那太麻烦了。当然可以有公共的方法。
基于AOPResponseBodyAdvice

名称 @RestControllerAdvice
类型 类注解
位置 Rest风格开发的控制器增强类定义上方
作用 为Rest风格开发的控制器类做增强

@RestControllerAdvice注解自带@ResponseBody注解与@Component注解,具备对应的功能。

示例代码:

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
package com.kakawanyifan.controller;

import com.kakawanyifan.pojo.Result;
import com.kakawanyifan.constant.ResultConstant;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.List;

@RestControllerAdvice
public class RestResultWrapper implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

if(body instanceof Boolean) {
Boolean flagBody = (Boolean) body;
Integer flag = flagBody ? ResultConstant.SUCCESS : ResultConstant.FAIL;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.FAIL_MSG;
return new Result(null,flag,flagMsg);
}else if (body instanceof List){
List<Object> objectList = (List<Object>) body;
Integer flag = (objectList.size() > 0) ? ResultConstant.SUCCESS : ResultConstant.SUCCESS_NODATA;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.SUCCESS_NODATA_MSG;
return new Result(objectList,flag,flagMsg);
}else {
Integer flag = body != null ? ResultConstant.SUCCESS : ResultConstant.SUCCESS_NODATA;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.SUCCESS_NODATA_MSG;
return new Result(body,flag,flagMsg);
}
}
}
  • supports,选择哪些类,或哪些方法需要走beforeBodyWrite,例如:
    • returnType.getMethod().getDeclaringClass().getName(),值为com.kakawanyifan.controller.BookController
    • returnType.getMethod().getName(),值为getById

异常处理

现象描述

现在,我们修改我们的程序的getById(@PathVariable Integer id)方法,添加id = id / 0;

1
2
3
4
5
6
7
8
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
id = id / 0;
Book book = bookService.getById(id);
Integer flag = book != null ? ResultConstant.SUCCESS : ResultConstant.SUCCESS_NODATA;
String flagMsg = (flag == ResultConstant.SUCCESS) ? ResultConstant.SUCCESS_MSG : ResultConstant.SUCCESS_NODATA_MSG;
return new Result(book,ResultConstant.GET,flag,flagMsg);
}

这时候,我们再请求。

异常

这个存在两个问题:

  1. 返回的报文不规范,前端或其他调用方可能解析报错。
  2. 返回的报文中,还包括了我们程序内部的很多信息,从业务角度,存在隐患。

异常处理规则

在我们的程序中,可能会在哪些地方出现异常?

  • 框架内部
    因使用不合规导致,例如框架的配置文件错了。
  • 数据层
    因使用不合规导致,例如SQL写错了。
    因外部服务器故障导致,例如数据库连接超时。
  • 业务层
    因业务逻辑书写错误导致,例如业务层进行数据处理的时候,没有判断空。
  • 表现层
    因数据收集、校验等规则导致,例如不匹配的数据类型间导致异常。
  • 工具类
    因工具类书写不够健壮导致,例如连接长期未释放等。

我们的处理规则是:

  1. 所有的异常均抛出到表现层进行处理。
  2. 对异常进行分类,分类处理。

都在表现层处理的话,那我们岂不是需要在表现层的每一个方法中都写上相关的代码?
当然不是,同样我们利用AOP。同样,SpringMVC已经为我们提供了解决方案。

异常处理器

名称 @ExceptionHandler
类型 方法注解
位置 专用于异常处理的控制器方法上方
作用 设置指定异常的处理方案,功能等同于控制器方法。
出现异常后终止原始控制器执行,并转入当前方法执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan.controller;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ProjectExceptionAdvice {

@ExceptionHandler(Exception.class)
public void doException(Exception ex){
ex.printStackTrace();
}

}

并且,我们需要在上文的RestResultWrappersupports方法中,排除异常处理器类。示例代码:

1
2
3
4
5
6
7
8
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if ("com.kakawanyifan.controller.ProjectExceptionAdvice".equals(returnType.getMethod().getDeclaringClass().getName()) &&
"doException".equals(returnType.getMethod().getName())) {
return false;
}
return true;
}

然后我们进行测试。

运行程序,测试

运行结果:

1
2
3
4
5
java.lang.ArithmeticException: / by zero
at com.kakawanyifan.controller.BookController.getById(BookController.java:42)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

【部分运行结果略】

异常已经被拦截并执行了doException方法。
但是前端收到的返回体是空的啊,这不行,修改如下:

1
2
3
4
5
@ExceptionHandler(Exception.class)
public Result doException(Exception ex){
ex.printStackTrace();
return new Result(null, ResultConstant.FAIL,ResultConstant.FAIL_MSG);
}

项目异常处理

异常分类

在一个项目中,异常通常分为一下几类:

  • 业务异常(Business Exception)
    不规范的用户行为产生的异常
  • 系统异常(System Exception)
    项目运行过程中可预计但无法避免的异常,比如数据库或服务器宕机。
  • 其他异常(Exception)
    未预期到的异常

将异常分类以后,针对不同类型的异常,要提供具体的解决方案:

处理异常

步骤:

  1. 自定义异常
    自定义两个类,SystemExceptionBusinessException,继承RuntimeException
  2. 将其他异常包成自定义异常
    可以在try{} catch(){} catch中重新throw我们自定义异常。
    可以直接throw自定义异常即可,例如:
    1
    2
    3
    if(【用户名密码不对】){
    throw new BusinessException(【用户名密码不对】);
    }
  3. 用统一的方法去处理异常。

示例代码:

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
package com.kakawanyifan.controller;

import com.kakawanyifan.constant.ResultConstant;
import com.kakawanyifan.exception.BusinessException;
import com.kakawanyifan.exception.SystemException;
import com.kakawanyifan.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ProjectExceptionAdvice {

@ExceptionHandler(SystemException.class)
public Result doSystemException(SystemException ex){
// TODO: 2022/12/11 记录日志
// TODO: 2022/12/11 发送告警
return new Result(null,null, ResultConstant.FAIL,ResultConstant.FAIL_MSG);
}

@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException ex){
// TODO: 2022/12/11 记录日志
// TODO: 2022/12/11 发送告警
return new Result(null,null, ResultConstant.FAIL,ResultConstant.FAIL_MSG);
}

@ExceptionHandler(Exception.class)
public Result doException(Exception ex){
ex.printStackTrace();
return new Result(null,null, ResultConstant.FAIL,ResultConstant.FAIL_MSG);
}

}

异常处理小结

返回规范报文

模块化

优点

  1. 方便重用
    例如当我们需要再开发一条电商线的时候,我们只需要引用"base"、"product"等,这些包都是复用的,是我们平台复用的基础类库,供所有的项目使用。
    这也模块化最重要的一个目的。
  2. 提高灵活性
    比如我们的公共的jar包,“base”、"product"等,我们不需要再下载源码,只需要发布到nexus,其他人从nexus下载即可。代码的可维护性、可扩展性好,并且保证了项目独立性与完整性。

实现

抽取POJO层

创建新模块

  1. 点击创建新模版。
    创建新模块
  2. 注意,此时不要点击通过模板创建
    不要点击通过模板创建
  3. Parent选择NoneLocation的路径也改一下,需要和原项目在同一层级的目录下。
    选择,的路径改一下

在新模块中创建相关的包以及类

我们原来的Book类用了lombok,在这里我们暂时去掉,用原生的自己写Getter和Sett的方法。

新模块中创建相关的包以及类

在原项目中添加新模块的依赖。

我们在原项目SSM中删除Book这个POJO。然后,添加新模块的依赖。

1
2
3
4
5
<dependency>
<groupId>com.kakawanyifan</groupId>
<artifactId>ssm-pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

将项目安装本地仓库

但如果我们此时直接编译原项目会报错。

运行结果:

1
2
3
4
5
6

【部分运行结果略】

[ERROR] Failed to execute goal on project ssm: Could not resolve dependencies for project com.kakawanyifan:ssm:war:1.0-SNAPSHOT: Could not find artifact com.kakawanyifan:ssm-pojo:jar:1.0-SNAPSHOT -> [Help 1]

【部分运行结果略】

因为找不到ssm-pojo这个jar包。
我们还需要先通过maven的install命令,把其安装到Maven的本地仓库中。之后才可以编译原项目。

抽取Dao层

关于抽取Dao层,基本上一致。我们简单说一下步骤。

  1. 创建新模块
  2. 在新模块中创建相关的包以及类
  3. 在创建好后,我们会发现缺失了很多包。包括Book类所在包ssm-pojo
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>com.kakawanyifan</groupId>
    <artifactId>ssm-pojo</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.11</version>
    </dependency>
  4. 在原项目中添加新模块的依赖
  5. 将项目安装本地仓库

我们可能会看到这个提示。

bookDao

这是因为BookDao是一个接口,没有实现类,接口是不能创建对象的,最终注入的是代理对象;代理对象是由Spring的IoC容器来创建管理的;IoC容器又是在Web服务器启动的时候才会创建;IDEA在检测依赖关系的时候,没有找到适合的类注入,所以会有提示;但是程序运行的时候,代理对象就会被创建,框架会进行注入,所以程序运行无影响。

如果觉得不舒服,可以将此处的改为Warning

Warning

依赖管理

传递

定义

依赖是具有传递性。

依赖是具有传递性

假设A代表我们的项目;BCDEFG代表不同的依赖;D1D2E1E2代表是同一个依赖包的不同版本。

A依赖了BCBC又分别依赖了其他的包,所以在A项目中就可以使用上面所有包,这就是所谓的依赖传递。

相对于A来说,A直接依赖BC,间接依赖D1E1GFD2E2。这就是所谓直接依赖和间接依赖。同理,相对于B来说,B直接依赖了D1E1,间接依赖了G

也正因为有依赖传递,所以会导致包在依赖的过程中出现冲突问题,即项目依赖的某一个包,有多个不同的版本,因而造成类包版本冲突。

对于同时存在多个不同版本的包,Spring中有一个规则。
在实际工作中,一般没有加载上期望的版本或者加载了不想加载的包,才称为冲突。

层级浅的优先

当依赖中出现相同的资源时,层级越浅,优先级越高。

在我们上例中,A通过B间接依赖了E1A通过C,再通过D2,间接依赖了E2;即A间接依赖了E1E2
因为,E1是2度,E2是3度,所以最终会选择E1

先声明的优先

当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的。

在我们上例中,A通过B间接依赖了D1A通过C间接依赖了D2D1D2都是两度,如果我们先声明了B,则会使用的是D1

后配置的优先(同一个POM)

如图,在同一个POM文件中,我们同时依赖"2.14.1版本的jackson-databind"和"2.0.0的jackson-databind",会发现最后加载的是我们后配置的。

后配置的覆盖先配置的

查看依赖关系

在上文,我们讨论了依赖的传递。那么,岂不是我们每次都要分析依赖了那些包?
不用。
我们可以通过IDEA查看。

通过IDEA查看

特别的,IDEA还提供了图形工具,点击如下的按钮,即可查看。

可视化

可视化

可选依赖

可选依赖是隐藏当前工程所依赖的资源,隐藏后对应资源将不具有依赖传递。

例如,在上文,ssm项目,通过依赖ssm-dao,间接依赖ssm-pojo

如果我们在ssm-daopom.xml,对ssm-pojo添加optional,并设置为true呢?

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
<?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>com.kakawanyifan</groupId>
<artifactId>ssm-dao</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>com.kakawanyifan</groupId>
<artifactId>ssm-pojo</artifactId>
<version>1.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>

</dependencies>

</project>

此时,ssm-pojo将不会通过ssm-dao被传递给ssm,同时ssm项目也会出现相关的报错。

ssm

排除依赖

现在,换一下。
假如是ssm不想因为依赖了ssm-dao,导致间接依赖了ssm-pojo
那么可以用排除依赖。
ssmpom.xml排除ssm-pojo

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
<?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>com.kakawanyifan</groupId>
<artifactId>ssm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>

【部分代码略】

<dependency>
<groupId>com.kakawanyifan</groupId>
<artifactId>ssm-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.kakawanyifan</groupId>
<artifactId>ssm-pojo</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>

</project>

在实际工作中,曾经因为log4j存在漏洞,而可能我们依赖的某些其他组件依赖了log4j,导致我们的项目最终依赖了log4j,我们可以用这种方式进行修复。

聚合

在分模块开发后,我们需要将这四个项目都安装到本地仓库,我们通过Maven的install来安装;如果我们的项目足够多,那一个一个install确实很麻烦。
此外,在四个项目都已经安装成功后,如果ssm-pojo发生变化后,我们得将ssm-pojo重新安装到maven仓库,然后还需要将所有模块再install一遍。

所以我们就想能不能抽取一个项目,把所有的项目管理起来,以后再想操作这些项目,只需要操作我们抽取的这个项目,这样就省事多了
这就是聚合。

将多个模块组织成一个整体,同时进行项目构建的过程称为聚合。

创建聚合工程

聚合工程,通常是一个不具有业务功能的空工程。打包方式通常为pom

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>com.kakawanyifan</groupId>
<artifactId>s</artifactId>
<version>1.0-SNAPSHOT</version>
<!--设置打包方式-->
<packaging>pom</packaging>

</project>

pom.xml添加所要管理的项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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>com.kakawanyifan</groupId>
<artifactId>s</artifactId>
<version>1.0-SNAPSHOT</version>
<!--设置打包方式-->
<packaging>pom</packaging>

<modules>
<module>../ssm</module>
<module>../ssm-dao</module>
<module>../ssm-pojo</module>
</modules>

</project>

我们看到,每一个项目,都有../,因为相对于我们POM文件来说,的确在POM文件的上一级的同级目录。

目录结构

此外,IDEA也会给我们提示。

提示

使用聚合统一管理项目

在maven面板上点击compile,会发现所有受管理的项目都会被执行编译,这就是聚合工程的作用。

聚合工程管理的项目在进行运行的时候,会按照项目与项目之间的依赖关系来自动决定执行的顺序和配置的顺序无关。虽然我们配置的顺序是ssmssm-daossm-pojo。但是执行的时候按照依赖关系编译是ssm-pojossm-daossm

运行结果:

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
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] ssm-pojo [jar]
[INFO] ssm-dao [jar]
[INFO] ssm [war]
[INFO] s [pom]
[INFO]
[INFO] ---------------------< com.kakawanyifan:ssm-pojo >----------------------
[INFO] Building ssm-pojo 1.0-SNAPSHOT [1/4]
[INFO] --------------------------------[ jar ]---------------------------------

【部分运行结果略】

[INFO] ----------------------< com.kakawanyifan:ssm-dao >----------------------
[INFO] Building ssm-dao 1.0-SNAPSHOT [2/4]
[INFO] --------------------------------[ jar ]---------------------------------

【部分运行结果略】

[INFO] ------------------------< com.kakawanyifan:ssm >------------------------
[INFO] Building ssm 1.0-SNAPSHOT [3/4]
[INFO] --------------------------------[ war ]---------------------------------

【部分运行结果略】

[INFO] -------------------------< com.kakawanyifan:s >-------------------------
[INFO] Building s 1.0-SNAPSHOT [4/4]
[INFO] --------------------------------[ pom ]---------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for s 1.0-SNAPSHOT:
[INFO]
[INFO] ssm-pojo ........................................... SUCCESS [ 1.023 s]
[INFO] ssm-dao ............................................ SUCCESS [ 0.074 s]
[INFO] ssm ................................................ SUCCESS [ 0.928 s]
[INFO] s .................................................. SUCCESS [ 0.001 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.108 s
[INFO] Finished at: 2022-12-13T07:59:47+08:00
[INFO] ------------------------------------------------------------------------

Process finished with exit code 0

继承

多模块开发还存在的另外一个问题:重复配置。

例如,我们在ssm模块中引用了mybatis3.5.11版本的,在ssm-dao也引用了mybatis,也是3.5.11版本的。
如果后期想升级mybatis版本,所有相关的项目都得改,针对这些问题,我们就得用到接下来要学习的继承。

继承:描述的是两个工程间的关系。与Java类中的继承类似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承。

创建父工程

继承的父工程,通常也是一个空的Maven项目,打包方式为pom
在本文,我们直接利用上一步的s

在子工程中设置其父工程

配置自工程继承自父工程。

通过<parent>标签定义继承关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>com.kakawanyifan</groupId>
<artifactId>ssm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<parent>
<groupId>com.kakawanyifan</groupId>
<artifactId>s</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../s/pom.xml</relativePath>
</parent>

【部分代码略】

</project>
  • 注意,relativePath不能省略。

优化子项目共有依赖的管理

将子项目共同使用的包都抽取出来,维护在父项目的pom.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
<?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>com.kakawanyifan</groupId>
<artifactId>s</artifactId>
<version>1.0-SNAPSHOT</version>
<!--设置打包方式-->
<packaging>pom</packaging>

<modules>
<module>../ssm</module>
<module>../ssm-dao</module>
<module>../ssm-pojo</module>
</modules>

<dependencies>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.24</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.24</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.24</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>

</dependencies>

</project>

删除子项目中已经被抽取到父项目的pom.xml中的jar包。
删除完后,我们会发现父项目中有依赖对应的jar包,子项目虽然已经将重复的依赖删除掉了,但是通过Maven面板看,子项目中所需要的包依然存在。

dependencyManagement

我们把所有的包都管理在父项目的pom.xml,看上去确实简单些,但这样就会导致有很多项目引入了过多不需要的包。

例如,我们看到,哪怕是ssm-pojo,也引入包括Spring在内的诸多其用不到的jar包。

引用了过多的JAR包

我们利用dependencyManagement标签,这样父工程只做依赖管理,不实际进行依赖。子项目要想使用它所提供的这些jar包,需要自己添加依赖,并且不需要指定<version>

这样的话,子工程中使用父工程中的可选依赖时,无需再指定版本,版本由父工程统一提供,避免版本冲突。
当然,子工程中还是可以定义父工程中没有定义的依赖关系,这些依赖不能被父工程进行版本统一管理。

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
<?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>com.kakawanyifan</groupId>
<artifactId>s</artifactId>
<version>1.0-SNAPSHOT</version>
<!--设置打包方式-->
<packaging>pom</packaging>

<modules>
<module>../ssm</module>
<module>../ssm-dao</module>
<module>../ssm-pojo</module>
</modules>

<dependencyManagement>
<dependencies>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.24</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

【部分代码略】

</dependencies>
</dependencyManagement>


</project>

属性管理(版本)

现象描述

在上文,我们的spring-webmvcspring-jdbcspring-test都是5.3.24版本的。
如果我们需要进行Spring的升级,按现在的项目,需要修改三处。
有没有办法把版本也抽出来?

定义属性(版本)

在父工程的pom文件中,我们定义属性。

1
2
3
4
<properties>
<spring.version>5.3.24</spring.version>
<mybatis.version>3.5.11</mybatis.version>
</properties>

修改依赖的version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
  • 使用<properties>标签来定义属性,在<properties>标签内自定义标签名当做属性名,自定义标签内的值即为属性值。
    例如,<spring.version>5.3.24</spring.version>,属性名为spring.version,属性值为5.3.24,在其他地方引用变量时用${变量名}

多环境

属性管理(配置)

在上文,我们通过Maven来集中管理Jar包的版本。
那么,有没有办法让Maven来管理我们的配置呢?例如jdbc.properties

定义属性

在父工程中定义属性。

1
2
3
4
5
6
7
<properties>
<spring.version>5.3.24</spring.version>
<mybatis.version>3.5.11</mybatis.version>
<jdbc.url>jdbc:mysql://127.0.0.1:3306/ssm</jdbc.url>
<jdbc.username>root</jdbc.username>
<jdbc.password>MySQL@2022</jdbc.password>
</properties>

jdbc.properties文件中引用属性

jdbc.properties在子工程SSM中。

1
2
3
jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}

设置maven过滤文件范围

在父工程的pom文件里配置。

1
2
3
4
5
6
7
8
9
10
<build>
<resources>
<!--设置资源目录-->
<resource>
<directory>../ssm/src/main/resources</directory>
<!--设置能够解析${},默认是false -->
<filtering>true</filtering>
</resource>
</resources>
</build>

注意,配置的是目录../ssm/src/main/resources,而不是某个具体的文件,不是../ssm/src/main/resources/jdbc.properties

测试是否生效

我们可以找到ssm项目的war包,然后解压,查看其jdbc.properties。发现已经生效了。

管理多个子项目的

如果有多个子项目都需要按照这种方式配置呢?

一个一个配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<resources>
<!--设置资源目录-->
<resource>
<directory>../ssm/src/main/resources</directory>
<!--设置能够解析${},默认是false -->
<filtering>true</filtering>
</resource>
<resource>
<directory>../ssm-pojo/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

统一配置

1
2
3
4
5
6
7
8
9
10
<build>
<resources>
<!--设置资源目录-->
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<!--设置能够解析${},默认是false -->
<filtering>true</filtering>
</resource>
</resources>
</build>
  • ${project.basedir}: 当前项目所在目录。因为我们的子项目继承了父项目,所以相当于所有的子项目都添加了资源目录的过滤。

说如果打包过程中出现错误Error assembling WAR: webxml attribute is required
因为,Maven发现我们的项目为web项目,就会去找web项目的入口web.xml(配置文件配置的方式),发现没有找到,就会报错。

我们可以在ssm项目的src\main\webapp\WEB-INF\添加一个web.xml文件,内容如下:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>

也可以配置Maven打包war时,忽略web.xml检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<resources>
【部分代码略】
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>

Maven属性

上面我们使用的${project.basedir}其实就是的Maven的内置属性。

Maven中的属性有:

属性分类引用格式示例
自定义属性${自定义属性名}${spring.vension}
内置属性${内置属性名}${basedir}${version}
setting属性${setting.属性名}${settings.localRepository}
Java系统属性${系统属性分类.系统属性名}${user.home}
环境变量属性${env.环境变量属性名}${env.JAVA_HOME}

多环境配置

多环境配置,其实利用的就是我们刚刚讨论的"属性管理(配置)"。

定义环境

我们利用<profiles>标签,在父工程定义多个环境,并指定默认环境。

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
<profiles>
<profile>
<id>dev</id>
<properties>
<jdbc.url>jdbc:mysql://127.0.0.1:3306/ssm</jdbc.url>
<jdbc.username>root</jdbc.username>
<jdbc.password>MySQL@2022</jdbc.password>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>sit</id>
<properties>
<jdbc.url>【SIT地址】</jdbc.url>
<jdbc.username>【SIT用户名】</jdbc.username>
<jdbc.password>【SIT密码】</jdbc.password>
</properties>
</profile>
<profile>
<id>prd</id>
<properties>
<jdbc.url>【PRD地址】</jdbc.url>
<jdbc.username>【PRD用户名】</jdbc.username>
<jdbc.password>【PRD密码】</jdbc.password>
</properties>
</profile>
</profiles>

如下的标签,是在指定默认环境。

1
2
3
4
<!--设定是否为默认环境-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>

打生产的包

如果我们想打生产的包,怎么办?

  1. 修改POM文件,定义生产环境为默认环境。
  2. 通过命令,指定环境。
    例如:mvn install -P prd,命令需要在pom.xml所在目录下进行执行。

跳过测试

上文在install的时候,Maven都会按照顺序从上往下依次执行,每次都会执行test。
如果想跳过test,有两种方法。

IDEA工具跳过测试

IDEA的Maven面板上有一个按钮,点击之后可以跳过测试,不过此种方式会跳过所有的测试。

IDEA工具实现跳过测试

如果我们想更精细的控制哪些跳过,哪些不跳过,那么就需要使用配置插件的方式来完成了

配置插件实现跳过测试

在父工程中的pom.xml中添加测试插件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<build>
<resources>

【部分代码略】

</resources>
<plugins>

【部分代码略】

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
<!--排除掉不参与测试的内容-->
<excludes>
<exclude>**/BookServiceTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
  • skipTests:如果为true,则跳过所有测试,如果为false,则不跳过测试。
  • excludes:哪些测试类不参与测试,即排除,针对skipTestsfalse来设置的
  • includes:哪些测试类要参与测试,即包含,针对skipTeststrue来设置的

命令行跳过测试

使用Maven的命令行,mvn 指令 -D skipTests

发布

Maven私服

安装Nexus

Maven私服:公司内部搭建的用于存储Maven资源的服务器。

搭建Maven私服的方式有很多,我们这里以Nexus为例。
下载地址如下:https://help.sonatype.com/repomanager3/product-information/download

私服

我们发现居然没有Linux版本的。网上资料,有在Linux上安装OSX版本的,有在UNIX上安装Linux版本的。但是至少官方是没有Linux版本的。
我个人也不建议不要在Linux系统上安装其他操作系统的版本。在这里,我们暂且以在Windows版本为例。

下载后,进行解压,会找到这两个目录。

两个目录

两个都需要。

启动Nexus

nexus-3.43.0-01\bin目录,输入命令

1
nexus.exe /run nexus

直到打印如下内容,说明启动成功。

1
2
3
4
5
-------------------------------------------------

Started Sonatype Nexus OSS 3.43.0-01

-------------------------------------------------

在浏览器输入http://127.0.0.1:8081/访问管理页面。
如果想修改启动端口的话,可以通过修改配置文件nexus-3.43.0-01\etc\nexus-default.properties进行更改。

在右上角点击登录,如果是初次登录,会弹框如下:

登录

在提示的页面找到用户名和密码,完成初始的配置。

私服仓库分类

所有私服仓库总共分为三大类:

  • 宿主仓库(hosted)
    保存无法从中央仓库获取的资源
    例如:自主研发的项目,某些第三方的项目(例如Oracle数据库连接的JAR包,必须从Oracle官网下载,中央仓库没有)。
  • 代理仓库(proxy)
    通过nexus访问其他公共仓库,例如访问中央仓库。
  • 仓库组(group)
    将若干个仓库组成一个群组,仓库组不能保存资源,属于抽象型仓库。

新建仓库

我们在私服上新建两个仓库,分别是:

  • kaka-release
  • kaka-snapshot

点击箭头所指位置,新建仓库。

新建仓库

Select Recipe选择maven2(hosted)

maven2(hosted)

Version policy,分别选对应的ReleaseSnapshot

Version policy

假如仓库组

如图,点击仓库组。
maven-public

在页面底部,把我们新建的仓库加入到仓库组。

假如仓库组

Maven设置

配置本地Maven的权限

修改本地Maven的配置文件。

在本文,配置文件位于/usr/local/maven/apache-maven-3.8.5/conf/settings.xml

<servers>标签下,新增两个<server>

1
2
3
4
5
6
7
8
9
10
11
<server>
<id>kaka-release</id>
<username>admin</username>
<password>Nexus14.</password>
</server>

<server>
<id>kaka-snapshot</id>
<username>admin</username>
<password>Nexus14.</password>
</server>

配置私服的访问路径

1
2
3
4
5
6
7
8
<mirror>
<!--配置仓库组的ID-->
<id>maven-public</id>
<!--*代表所有内容都从私服获取-->
<mirrorOf>*</mirrorOf>
<!--私服仓库组maven-public的访问路径-->
<url>http://10.211.55.8:8081/repository/maven-public/</url>
</mirror>
  • id:配置仓库组的ID。
  • mirrorOf*,所有内容都从私服获取。
  • url:私服仓库组maven-public的访问路径。

配置工程

配置工程上传私服的具体位置

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
<?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>com.kakawanyifan</groupId>
<artifactId>s</artifactId>
<version>1.0-SNAPSHOT</version>
<!--设置打包方式-->
<packaging>pom</packaging>

<modules>
<module>../ssm</module>
<module>../ssm-dao</module>
<module>../ssm-pojo</module>
</modules>

【部分代码略】

<distributionManagement>
<repository>
<id>kaka-release</id>
<url>http://10.211.55.8:8081/repository/kaka-release/</url>
</repository>
<snapshotRepository>
<id>kaka-snapshot</id>
<url>http://10.211.55.8:8081/repository/kaka-snapshot/</url>
</snapshotRepository>
</distributionManagement>


</project>
  • 要发布的项目都需要配置distributionManagement标签。或者在项目自身的pom.xml中配置;要么在其父项目中配置,子项目继承父项目。

发布到私服

执行deploy,即可发布到私服。

如果报如下的错误:

1
authentication failed for http://10.211.55.8:8081/repository/kaka-snapshot/com/kakawanyifan/s/1.0-SNAPSHOT/s-1.0-20221213.150358-1.pom, status: 401 Unauthorized

检查IDEA的Maven的配置文件。

IDEA的Maven的配置文件

发布成功后,我们会在相关的仓库中看到。

发布成功后,我们会在相关的仓库中看到

如果想发布到kaka-release仓库,我们需要将pom.xml中的version改成RELEASE

1
2
<!--<version>1.0-SNAPSHOT</version>-->
<version>1.0-RELEASE</version>

其他私服配置

中央仓库

如果私服中没有对应的包,会去中央仓库下载,默认的中央仓库访问较慢,可以配置让私服去阿里云中下载依赖。

修改maven-centralRemote storage

修改的

找回密码

如果Nexus密码忘了,可以参考如下的步骤重置密码:

  1. 停止nexus服务,直接杀进程,或者是./nexus stop
  2. 访问nexus内置的数据库
    1
    2
    cd nexus/lib/
    java -jar support/nexus-orient-console.jar
  3. 登录数据库,密码为admin
    1
    connect plocal:../../sonatype-work/nexus3/db/security/admin
    • 注意执行的位置,确定../层级
  4. 执行如下的SQL,将admin用户密码重置为admin123
    1
    update user SET password="$shiro1$SHA-512$1024$NE+wqQq/TmjZMvfI7ENh/g==$V4yPw8T64UQ6GfJfxYq2hLsVrBY8D1v+bktfOxGdt4b/9BthpWPNUy/CBk6V9iA0nHpzYzJFWO8v/tZFtES8CA==" UPSERT WHERE id="admin"
  5. 退出
    1
    exit;
  6. 重启nexus服务,直接通过admin/admin123登录nexus
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10818
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区