https://blog.didispace.com/spring-boot-learning-1x/
1. 应用的统一异常处理
Spring Boot提供了一个默认的映射:/error
,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容。
访问一个不存在的URL,或是修改处理内容,直接抛出异常,如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
@RequestMapping("/hello")
public String hello() throws Exception {
throw new Exception("发生错误");
}
似下面的报错页面,该页面就是Spring Boot提供的默认error映射页面。
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Mar 01 11:16:04 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
1. 统一的异常处理类
异常拦截器
- 创建全局异常处理类:通过使用
@ControllerAdvice
定义统一的异常处理类,而不是在每个Controller中逐个定义。 @ExceptionHandler
用来定义函数针对的异常类型,最后将Exception对象和请求URL映射到error.html
中
//控制器切面
@ControllerAdvice
class GlobalExceptionHandler {
// View name
public static final String DEFAULT_ERROR_VIEW = "error";
//所有的 异常都进行拦截
@ExceptionHandler(value = Exception.class)
//HttpServletRequest
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
//创建 模型和视图
ModelAndView mav = new ModelAndView();
//异常放在 exception 对象里
mav.addObject("exception", e);
//添加url
mav.addObject("url", req.getRequestURL());
//view 的名字
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
Servlet
英 /ˈsɜːvlɪt/ 美 /'sɜvlet/ 全球(美国)
简明 新牛津 柯林斯 例句 百科
n. (尤指 Java 语言中在服务器上运行的)小型应用程序;小服务程序
网络释义 专业释义
控制器
控制器(Servlet)
故障页面返回
- 实现
error.html
页面展示:在templates
目录下创建error.html
,将请求的URL和Exception对象的message输出。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>统一异常处理</title>
</head>
<body>
<h1>Error Handler222</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>
启动该应用,访问:http://localhost:8080/hello
,可以看到如下错误提示页面。
Error Handler222
http://localhost:9898/testOne
通过实现上述内容之后,我们只需要在Controller
中抛出Exception
,
当然我们可能会有多种不同的Exception
。
然后在@ControllerAdvice
类中,根据抛出的具体Exception
类型匹配
@ExceptionHandler
中配置的异常类型来匹配错误映射和处理。
2. 测试普通的 controller到 index页面
@Controller
public class HelloController {
@RequestMapping("/")
public String index(ModelMap map) {
map.addAttribute("host", "http://blog.didispace.com");
return "index";
}
}
- src/main/resources/templates/index.html
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<h1>22222222</h1>
<h1 th:text="${host}">Hello World222</h1>
</body>
</html>
- 页面
22222222
http://blog.didispace.com
3. 返回JSON格式
本质上,只需在@ExceptionHandler
之后加入@ResponseBody
,就能让处理函数return的内容转换为JSON格式。
统一返回的类
- 创建统一的JSON返回对象,
- code:消息类型,
- message:消息内容,
- url:请求的url,
- data:请求返回的数据
public class ErrorInfo<T> {
public static final Integer OK = 0;
public static final Integer ERROR = 100;
//类型
private Integer code;
//内容
private String message;
//请求url
private String url;
//数据
private T data;
// 省略getter和setter
}
自定义异常
- 创建一个自定义异常,用来实验捕获该异常,并返回json
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
Controller
中增加json映射,抛出MyException
异常
@Controller
public class HelloController {
@RequestMapping("/json")
public String json() throws MyException {
throw new MyException("发生错误2");
}
}
异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = MyException.class)
@ResponseBody
public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception {
ErrorInfo<String> r = new ErrorInfo<>();
r.setMessage(e.getMessage());
r.setCode(ErrorInfo.ERROR);
r.setData("Some Data");
r.setUrl(req.getRequestURL().toString());
return r;
}
}
- 启动应用,访问:http://localhost:8080/json,可以得到如下返回内容:
{
code: 100,
data: "Some Data",
message: "发生错误2",
url: "http://localhost:8080/json"
}
2. 整合MyBatis
引入pom
- 这里用到spring-boot-starter基础和spring-boot-starter-test用来做单元测试验证数据访问
- 引入连接mysql的必要依赖mysql-connector-java
- 引入整合MyBatis的核心依赖mybatis-spring-boot-starter
- 这里不引入spring-boot-starter-jdbc依赖,是由于mybatis-spring-boot-starter中已经包含了此依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
</dependencies>
配置 mysql
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
CREATE TABLE `USER` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER,
`age` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
);
使用MyBatis
- 在Mysql中创建User表,包含id(BIGINT)、name(INT)、age(VARCHAR) 字段。同时,创建映射对象User
public class User {
private Long id;
private String name;
private Integer age;
// 省略getter和setter
}
UserMapper
@Mapper
public interface UserMapper {
@Select("SELECT * FROM USER WHERE NAME = #{name}")
User findByName(@Param("name") String name);
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
}
@SpringBootApplication
public class Application {
}
创建单元测试
- 测试逻辑:插入一条name=AAA,age=20的记录,然后根据name=AAA查询,并判断age是否为20
- 测试结束回滚数据,保证测试单元每次运行的数据环境独立
测试
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
@Rollback
public void findByName() throws Exception {
userMapper.insert("AAA", 20);
User u = userMapper.findByName("AAA");
Assert.assertEquals(20, u.getAge().intValue());
}
}
3. MyBatis注解配置详解
传参方式
下面通过几种不同传参方式来实现前文中实现的插入操作
使用@Param
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
@Param中定义的
name对应了SQL中的
#{name}
使用Map
如下代码,通过Map<String, Object>对象来作为传递参数的容器:
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER})")
int insertByMap(Map<String, Object> map);
因为参数是 Map<String,Object>,所以参数里,带上类型
INSERT INTO USER(NAME, AGE)
VALUES
(
#{name,jdbcType=VARCHAR},
#{age,jdbcType=INTEGER}
)
对于Insert语句中需要的参数,我们只需要在map中填入同名的内容即可,具体如下面代码所示:
Map<String, Object> map = new HashMap<>();
map.put("name", "CCC");
map.put("age", 40);
userMapper.insertByMap(map);
使用对象
除了Map对象,我们也可直接使用普通的Java对象来作为查询条件的传参,比如我们可以直接使用User对象:
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insertByUser(User user);
这样语句中的#{name}
、#{age}
就分别对应了User对象中的name
和age
属性。
增删改查
public interface UserMapper {
@Select("SELECT * FROM user WHERE name = #{name}")
User findByName(@Param("name") String name);
@Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
@Update("UPDATE user SET age=#{age} WHERE name=#{name}")
void update(User user);
@Delete("DELETE FROM user WHERE id =#{id}")
void delete(Long id);
}
在完成了一套增删改查后,不妨我们试试下面的单元测试来验证上面操作的正确性:
测试
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@Transactional
public class ApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
@Rollback
public void testUserMapper() throws Exception {
// insert一条数据,并select出来验证
userMapper.insert("AAA", 20);
User u = userMapper.findByName("AAA");
Assert.assertEquals(20, u.getAge().intValue());
// update一条数据,并select出来验证
u.setAge(30);
userMapper.update(u);
u = userMapper.findByName("AAA");
Assert.assertEquals(30, u.getAge().intValue());
// 删除这条数据,并select验证
userMapper.delete(u.getId());
u = userMapper.findByName("AAA");
Assert.assertEquals(null, u);
}
}
返回结果的绑定
而对于“查”操作,我们往往需要进行多表关联,汇总计算等操作,
往往需要返回一个与数据库实体不同的包装类,
就可以通过@Results
和@Result
注解来进行绑定,具体如下:
@Results({
@Result(property = "name", column = "name"),
@Result(property = "age", column = "age")
})
@Select("SELECT name, age FROM user")
List<User> findAll();
@Result中的property属性对应User对象中的成员名,
- column对应SELECT出的字段名。
- 在该配置中故意没有查出id属性,
- 只对User对应中的name和age对象做了映射配置,
- 这样可以通过下面的单元测试来验证查出的id为null,而其他属性不为null:
List<User> userList = userMapper.findAll();
for(User user : userList) {
Assert.assertEquals(null, user.getId());
Assert.assertNotEquals(null, user.getName());
}
4. Spring Boot中的事务管理
任何一步操作都有可能发生异常,异常会导致后续操作无法完成,
- 此时由于业务逻辑并未正确的完成,之前成功操作数据的并不可靠,需要在这种情况下进行回退。
事务的作用就是为了保证用户的每一个操作都是可靠的,
- 事务中的每一步操作都必须成功执行,
- 只要有发生异常就回退到事务开始 未进行操作的状态。
spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖的时候,框架会自动默认分别注入
- DataSourceTransactionManager或JpaTransactionManager。
- 所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。
引入了spring-data-jpa,并创建了User实体以及对User的数据访问对象UserRepository
测试用例
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
public void test() throws Exception {
// 创建10条记录
userRepository.save(new User("AAA", 10));
userRepository.save(new User("HHHHHHHHHH", 80));
userRepository.save(new User("BBB", 20));
// 省略后续的一些验证操作
}
}
通过定义User的name属性长度为5,这样通过创建时User实体的name属性超长就可以触发异常产生。
@Entity
//@Transactional
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, length = 5)
private String name;
@Column(nullable = false)
private Integer age;
// 省略构造函数、getter和setter
}
控制台中抛出了如下异常,name字段超长:
SQL Error: 1406, SQLState: 22001
Data truncation: Data too long for column 'name' at row 1
SQL Warning Code: 1406, SQLState: HY000
Data too long for column 'name' at row 1
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement
可以使用事务让它实现回退,做法非常简单,我们只需要在test函数上添加@Transactional
注解即可。
Rolled back transaction for test
context [DefaultTestContext@1d7a715 testClass = ApplicationTests, testInstance = com.didispace.ApplicationTests@95a785,
testMethod = test@ApplicationTests,
testException = org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a];
nested exception is org.hibernate.exception.DataException: could not execute statement,
mergedContextConfiguration = [MergedContextConfiguration@11f39f9 testClass = ApplicationTests,
locations = '{}', classes = '{class com.didispace.Application}', contextInitializerClasses = '[]',
activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}',
contextLoader = 'org.springframework.boot.test.SpringApplicationContextLoader', parent = [null]]].
再看数据库中,User表就没有AAA到GGG的用户数据了,成功实现了自动回滚
通常我们单元测试为了保证每个测试之间的数据独立,
- 会使用
@Rollback
注解让每个单元测试都能在结束时回滚。而真正在开发业务逻辑时, - 我们通常在service层接口中使用
@Transactional
来对各个业务逻辑进行事务管理的配置,例如:
public interface UserService {
@Transactional
User login(String name, String password);
}
事务详解
(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。对于不同数据源的事务管理配置可以见《Spring Boot多数据源配置与使用》中的设置。
- 在声明事务时,只需要通过value属性指定配置的事务管理器名即可,例如:
@Transactional(value="transactionManagerPrimary")
。
除了指定不同的事务管理器之后,还能对事务进行隔离级别和传播行为的控制,下面分别详细解释:
隔离级别
隔离级别是指
- 若干个并发的事务之间的隔离程度,
- 与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。
我们可以看org.springframework.transaction.annotation.Isolation
枚举类中定义了五个表示隔离级别的值:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
-
DEFAULT
:这是默认值,表示使用底层数据库的默认隔离级别。- 对大部分数据库而言,通常这值就是:
READ_COMMITTED
。
- 对大部分数据库而言,通常这值就是:
-
READ_UNCOMMITTED
:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。- 该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
-
READ_COMMITTED
:该隔离级别表示一个事务只能读取 另一个事务已经提交的数据。- 该级别可以防止脏读,这也是大多数情况下的推荐值。
-
REPEATABLE_READ
:该隔离级别表示一个事务在整个过程中- 可以多次重复执行某个查询,并且每次返回的记录都相同。
- 即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。
- 该级别可以防止脏读和不可重复读。
-
SERIALIZABLE
:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,- 也就是说,该级别可以防止脏读、不可重复读以及幻读。
- 但是这将严重影响程序的性能。通常情况下也不会用到该级别。
指定方法:通过使用isolation
属性设置,例如:
@Transactional(isolation = Isolation.DEFAULT)
isolation
英 /ˌaɪsəˈleɪʃn/ 美 /ˌaɪsəˈleɪʃn/ 全球(英国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n. 隔离,孤立;孤独;绝缘;
传播行为
所谓事务的传播行为是指,
- 如果在开始当前事务之前,一个事务上下文已经存在,
- 此时有若干选项可以指定一个事务性方法的执行行为。
我们可以看org.springframework.transaction.annotation.Propagation
枚举类中定义了6个表示传播行为的枚举值:
Propagation
英 /ˌprɒpəˈɡeɪʃn/ 美 /ˌprɑːpəˈɡeɪʃn/ 全球(美国)
简明 韦氏 例句 百科
n. (动植物等的)繁殖,增殖,;(观点、理论等的)传播;(运动、光线、声音等的)传送
mandatory
英 /ˈmændətəri/ 美 /ˈmændətɔːri/ 全球(美国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
adj. 强制性的,义务的;受(前国际联盟)委任统治的
n. 受托人,代理人(=mandatary)
nested
英 /ˈnestɪd/ 美 /ˈnestɪd/ 全球(英国)
简明 柯林斯 例句 百科
adj. 嵌套的,内装的
v. 筑巢;嵌入(nest 的过去分词)
public enum Propagation {
REQUIRED(0), //required 需要
SUPPORTS(1),//supports 支持
MANDATORY(2),//mandatory 强制
REQUIRES_NEW(3),//requires_new 我新开,挂起你
NOT_SUPPORTED(4),//not supported 不支持
NEVER(5),//never 从不
NESTED(6);//nested 嵌套
}
-
REQUIRED
:如果当前存在事务,则加入该事务;- 如果当前没有事务,则创建一个新的事务。
-
SUPPORTS
:如果当前存在事务,则加入该事务;- 如果当前没有事务,则以非事务的方式继续运行。
-
MANDATORY
:如果当前存在事务,则加入该事务;- 如果当前没有事务,则抛出异常。
-
REQUIRES_NEW
:创建一个新的事务,- 如果当前存在事务,则把当前事务挂起。
-
NOT_SUPPORTED
:以非事务方式运行,- 如果当前存在事务,则把当前事务挂起。
-
NEVER
:以非事务方式运行,- 如果当前存在事务,则抛出异常。
-
NESTED
:如果当前存在事务,- 则创建一个事务作为当前事务的嵌套事务来运行;
- 如果当前没有事务,则该取值等价于
REQUIRED
。
指定方法:通过使用propagation
属性设置,例如:
@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
5. 缓存支持(一)注解配置与EhCache使用
- 直接用 整合mybatis的例子,也行
往往数据库查询操作会成为影响用户使用体验的瓶颈,
- 此时使用缓存往往是解决这一问题非常好的手段之一。
为了更好的理解缓存,我们先对该工程做一些简单的改造。
application.properties
文件中新增spring.jpa.properties.hibernate.show_sql=true
,- 开启hibernate对sql语句的打印
- 修改单元测试
ApplicationTests
,初始化插入User表一条用户名为AAA,年龄为10的数据。并通过findByName函数完成两次查询。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;
@Before
public void before() {
userRepository.save(new User("AAA", 10));
}
@Test
public void test() throws Exception {
User u1 = userRepository.findByName("AAA");
System.out.println("第一次查询:" + u1.getAge());
User u2 = userRepository.findByName("AAA");
System.out.println("第二次查询:" + u2.getAge());
}
}
- 执行单元测试,我们可以在控制台中看到下面内容。
Hibernate: insert into user (age, name) values (?, ?)
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第一次查询:10
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第二次查询:10
在测试用例执行前,插入了一条User记录。然后每次findByName调用时,都执行了一句select语句来查询用户名为AAA的记录。
引入和配置 缓存
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
增加@EnableCaching
注解开启缓存功能
@SpringBootApplication
@EnableCaching
使用缓存
- 在数据访问接口中,增加缓存配置注解,如:
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable
User findByName(String name);
}
- 再来执行以下单元测试,可以在控制台中输出了下面的内容:
Hibernate: insert into user (age, name) values (?, ?)
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第一次查询:10
第二次查询:10
到这里,我们可以看到,在调用第二次findByName函数时,没有再执行select语句,也就直接减少了一次数据库的读取操作。
为了可以更好的观察,缓存的存储,我们可以在单元测试中注入cacheManager。
@Autowired
private CacheManager cacheManager;
使用debug模式运行单元测试,观察cacheManager中的缓存集users以及其中的User对象的缓存加深理解。
- 默认:ConcurrentMap CacheManager
- 底层:ConcurrentHashMap
Cache注解详解
@CacheConfig
:主要用于配置该类中会用到的一些共用的缓存配置。- 在这里
@CacheConfig(cacheNames = "users")
: - 配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,
- 我们也可以不使用该注解,直接通过
@Cacheable
自己配置缓存集的名字来定义。
- 在这里
@Cacheable
:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:value
、cacheNames
:两个等同的参数(cacheNames
为Spring 4新增,作为value
的别名),- 用于指定缓存存储的集合名。由于Spring 4中新增了
@CacheConfig
, - 因此在Spring 3中原本必须有的
value
属性,也成为非必需项了
- 用于指定缓存存储的集合名。由于Spring 4中新增了
key
:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的 所有参数组合 作为key值,- 若自己配置需使用SpEL表达式,比如:
@Cacheable(key = "#p0")
:使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档
- 若自己配置需使用SpEL表达式,比如:
condition
:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,- 比如:
@Cacheable(key = "#p0", condition = "#p0.length() < 3")
, - 表示只有当第一个参数的长度小于3的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。
- 比如:
unless
:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition
参数的地方在于它的判断时机,- 该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
keyGenerator
:用于指定key生成器,非必需。- 若需要指定一个自定义的key生成器,我们需要去实现
org.springframework.cache.interceptor.KeyGenerator
接口,并使用该参数来指定。 - 需要注意的是:该参数与
key
是互斥的
- 若需要指定一个自定义的key生成器,我们需要去实现
cacheManager
:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用cacheResolver
:用于指定使用那个缓存解析器,非必需。- 需通过
org.springframework.cache.interceptor.CacheResolver
接口来实现自己的缓存解析器,并用该参数指定。
- 需通过
除了这里用到的两个注解之外,还有下面几个核心注解:
-
@CachePut
:配置于函数上,能够根据参数定义条件来进行缓存,- 它与
@Cacheable
不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。 - 它的参数与
@Cacheable
类似,具体功能可参考上面对@Cacheable
参数的解析
- 它与
-
@CacheEvict :配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同
@Cacheable
一样的参数之外,它还有下面两个参数:
allEntries
:非必需,默认为false。当为true时,会移除所有数据beforeInvocation
:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
entries
英 /'entrɪs/ 美 /ˈentriz/ 全球(英国)
简明 柯林斯 例句 百科
n. 进入;(词典所列的)词目(entry 的复数形式)
invocation
英 /ˌɪnvəˈkeɪʃn/ 美 /ˌɪnvəˈkeɪʃn/ 全球(美国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n. (向神或权威人士的)求助,祈祷;咒语;(仪式或集会开始时的)发言,祷文;(法院对另案的)文件调取;(计算机)调用,启用;(法权的)行使
缓存配置
Spring Boot中到底使用了什么缓存呢?
在Spring Boot中通过@EnableCaching
注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
- Generic
- JCache (JSR-107)
- EhCache 2.x
- Hazelcast
- Infinispan
- Redis
- Guava
- Simple
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type
来强制指定。我们可以通过debug调试查看cacheManager对象的实例来判断当前使用了什么缓存。
本文中不对所有的缓存做详细介绍,下面以常用的EhCache为例,看看如何配置来使用EhCache进行缓存管理。
在Spring Boot中开启EhCache非常简单,只需要在工程中加入ehcache.xml
配置文件并在pom.xml中增加ehcache依赖,框架只要发现该文件,就会创建EhCache的缓存管理器。
- 在
src/main/resources
目录下创建:ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
</cache>
</ehcache>
- 在
pom.xml
中加入
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
完成上面的配置之后,再通过debug模式运行单元测试,观察此时CacheManager已经是EhCacheManager实例,说明EhCache开启成功了。
对于EhCache的配置文件也可以通过application.properties
文件中使用spring.cache.ehcache.config
属性来指定,比如:
spring.cache.ehcache.config=classpath:config/another-config.xml
5. 缓存支持(二)使用Redis做集中式缓存
EhCache是进程内的缓存框架,在集群模式下时,
- 各应用服务器之间的缓存都是独立的,因此在不同服务器的进程间会存在缓存不一致的情况。
- 即使EhCache提供了集群环境下的缓存同步策略,但是同步依然需要一定的时间,短暂的缓存不一致依然存在。
在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,就不能再使用EhCache来解决了,
- 这个时候使用集中式缓存是个不错的选择,因此本文将介绍如何在Spring Boot的缓存支持中使用Redis进行数据缓存。
准备工作
-
我使用的 整合mybatis的项目,也可以
-
引入了
spring-data-jpa
和EhCache
-
定义了
User
实体,包含id
、name
、age
字段 -
使用
spring-data-jpa
实现了对User
对象的数据访问接口UserRepository
-
使用
Cache
相关注解配置了缓存
开始改造
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
application.properties
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
Spring Boot会在侦测到存在Redis的依赖并且Redis的配置是可用的情况下,
- 使用
RedisCacheManager
初始化CacheManager
。
为此,我们可以单步运行我们的单元测试,可以观察到此时CacheManager
的实例是org.springframework.data.redis.cache.RedisCacheManager
,并获得下面的执行结果:
Hibernate: insert into user (age, name) values (?, ?)
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第一次查询:10
第二次查询:10
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: update user set age=?, name=? where id=?
第三次查询:10
可以观察到,在第一次查询的时候,执行了select语句;第二次查询没有执行select语句,说明是从缓存中获得了结果;
而第三次查询,我们获得了一个错误的结果,根据我们的测试逻辑,
- 在查询之前我们已经将age更新为20,但是我们从缓存中获取到的age还是为10。
注意点
类一定得序列化
User implements Serializable {}
redis中默认的 键值:users~keys
必须要起名字:@CacheConfig(cacheNames = “users”)
6. 转入作者的JPA
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
boot-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
test
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
mysql
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
properties
@SpringBootApplication
@EnableCaching
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop
spring.jpa.properties.hibernate.show_sql=true
User和 dao
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer age;
}
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable(key = "#p0", condition = "#p0.length() < 10")
User findByName(String name);
}
测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;
@Autowired
private CacheManager cacheManager;
@Before
public void before() {
userRepository.save(new User("AAA", 10));
}
@Test
public void test() throws Exception {
User u1 = userRepository.findByName("AAA");
System.out.println("第一次查询:" + u1.getAge());
User u2 = userRepository.findByName("AAA");
System.out.println("第二次查询:" + u2.getAge());
u1.setAge(20);
userRepository.save(u1);
User u3 = userRepository.findByName("AAA");
System.out.println("第三次查询:" + u3.getAge());
}
}
问题思考
为什么同样的逻辑在EhCache中没有问题,但是到Redis中会出现这个问题呢?
在EhCache缓存时没有问题,主要是由于EhCache是进程内的缓存框架,
- 第一次通过select查询出的结果被加入到EhCache缓存中,
- 第二次查询从EhCache取出的对象与第一次查询对象实际上是同一个对象(可以在使用Chapter4-4-1工程中,观察u1==u2来看看是否是同一个对象),
- 因此我们在更新age的时候,实际已经更新了EhCache中的缓存对象。
而Redis的缓存独立存在于我们的Spring应用之外,
- 我们对数据库中数据做了更新操作之后,没有通知Redis去更新相应的内容,
- 因此我们取到了缓存中未修改的数据,导致了数据库与缓存中数据的不一致。
因此我们在使用缓存的时候,要注意缓存的生命周期,利用好上一篇上提到的几个注解来做好缓存的更新、删除
进一步修改
针对上面的问题,我们只需要在更新age的时候,通过@CachePut
来让数据更新操作同步到缓存中,就像下面这样:
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable(key = "#p0")
User findByName(String name);
@CachePut(key = "#p0.name")
User save(User user);
}
在redis-cli中flushdb,清空一下之前的缓存内容,再执行单元测试,可以获得下面的结果:
Hibernate: insert into user (age, name) values (?, ?)
第一次查询:10
第二次查询:10
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: update user set age=?, name=? where id=?
第三次查询:20
可以看到,我们的第三次查询获得了正确的结果!
- 同时,我们的第一次查询也不是通过select查询获得的,
- 因为在初始化数据的时候,调用save方法时,就已经将这条数据加入了redis缓存中,
- 因此后续的查询就直接从redis中获取了。
一定要注意缓存生命周期的控制,防止数据不一致的情况出现。
-
redis的key说明
users~keys 默认的,应该是存所有的key吧 \xAC\xED\x00\x05t\x00\x03AAA user的name开始的 key
7. boot 1.5 动态修改日志
Spring Boot 1.5.x中引入的一个新的控制端点:/loggers
动态修改Spring Boot应用日志级别的强大功能
pom
parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
测试类
@RestController
@SpringBootApplication
public class DemoApplication {
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/test", method = RequestMethod.GET)
public String testLogLevel() {
logger.debug("Logger Level :DEBUG");
logger.info("Logger Level :INFO");
logger.error("Logger Level :ERROR");
return "";
}
}
进行配置
- 为了后续的试验顺利,在
application.properties
中增加一个配置,来关闭安全认证校验。
management.security.enabled=false
不然在访问/loggers
端点的时候,会报如下错误:
{
"timestamp": 1485873161065,
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource.",
"path": "/loggers/com.didispace"
}
测试验证
在完成了上面的构建之后,我们启动示例应用,并访问/test
端点,我们可以在控制台中看到如下输出:
Logger Level :INFO
Logger Level :ERROR
由于默认的日志级别为INFO
,所以并没有输出DEBUG
级别的内容。下面我们可以尝试通过/logger
端点来将日志级别调整为DEBUG
,比如,发送POST请求到/loggers/com.didispace
端点,其中请求体Body内容为:
{
"configuredLevel": "DEBUG"
}
重新访问/test
端点,我们将在控制台中看到如下输出,在/test
端点中定义的DEBUG
日志内容被打印了出来:
Logger Level :DEBUG
Logger Level :INFO
Logger Level :ERROR
以通过GET请求来查看当前的日志级别设置,比如:发送GET请求到/loggers/com.didispace
端点,我们将获得对于com.didispace
包的日志级别设置:
{
"configuredLevel": "DEBUG",
"effectiveLevel": "DEBUG"
}
我们也可以不限定条件,直接通过GET请求访问/loggers
来获取所有的日志级别设置
8. RabbitMq
使用docker
拉取镜像
docker pull rabbitmq:3-management
启动镜像(默认用户名密码),默认guest 用户,密码也是 guest
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management
docker run -d
--hostname my-rabbit
--name rabbit
-p 15672:15672
-p 5672:5672
rabbitmq:3-management
启动镜像(设置用户名密码)
--name rabbit 后面加上
-e RABBITMQ_DEFAULT_USER=user
-e RABBITMQ_DEFAULT_PASS=password
完成后访问:http://localhost:15672/
- 如果已经安装,使用下面方法
docker exec -it c5c4eaf0bfd7 /bin/bash
rabbitmqctl add_user mhlevel mhlevel #添加用户,后面两个参数分别是用户名和密码
rabbitmqctl set_permissions -p / mhlevel ".*" ".*" ".*" #添加权限
rabbitmqctl set_user_tags mhlevel administrator #修改用户角色
Message Broker与AMQP
Message Broker是一种消息验证、传输、路由的架构模式,其设计目标主要应用于下面这些场景:
- 消息路由到一个或多个目的地
- 消息转化为其他的表现方式
- 执行消息的聚集、消息的分解,并将结果发送到他们的目的地,然后重新组合相应返回给消息用户
- 调用Web服务来检索数据
- 响应事件或错误
- 使用发布-订阅模式来提供内容或基于主题的消息路由
AMQP是Advanced Message Queuing Protocol的简称,它是一个面向消息中间件 的开放式标准 应用层协议。AMQP定义了这些特性:
- 消息方向
- 消息队列
- 消息路由(包括:点到点和发布-订阅模式)
- 可靠性
- 安全性
Queuing
英 /ˈkjuːɪŋ/ 美 /ˈkjuːɪŋ/ 全球(英国)
简明 例句 百科
n. [数]排队;排队论
queue
英 /kjuː/ 美 /kjuː/ 全球(英国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n. <英>(人、汽车等的)队,行列;<英>(为得到某机会而等待的)长列,长队;(计算机)队列;呼叫队列;<古>辫子
v. <英>排队(等候);竞相,抢着(做某事);(计算机)排成队列,排队
RabbitMQ
本文要介绍的RabbitMQ就是以AMQP协议实现的一种中间件产品,它可以支持多种操作系统,多种编程语言,几乎可以覆盖所有主流的企业级技术平台。
安装
下面我们采用的Erlang和RabbitMQ Server版本说明:
- Erlang/OTP 19.1
- RabbitMQ Server 3.6.5
Windows安装
- 安装Erland,通过官方下载页面
http://www.erlang.org/downloads
获取exe安装包,直接打开并完成安装。 - 安装RabbitMQ,通过官方下载页面
https://www.rabbitmq.com/download.html
获取exe安装包。 - 下载完成后,直接运行安装程序。
- RabbitMQ Server安装完成之后,会自动的注册为服务,并以默认配置启动起来。
Mac OS X安装
在Mac OS X中使用brew工具,可以很容易的安装RabbitMQ的服务端,只需要按如下命令操作即可:
- brew更新到最新版本,执行:brew update
- 安装Erlang,执行:
brew install erlang
- 安装RabbitMQ Server,执行:
brew install rabbitmq
通过上面的命令,RabbitMQ Server的命令会被安装到/usr/local/sbin
,并不会自动加到用户的环境变量中去,所以我们需要在.bash_profile
或.profile
文件中增加下面内容:
PATH=$PATH:/usr/local/sbin
这样,我们就可以通过rabbitmq-server
命令来启动RabbitMQ的服务端了。
Ubuntu安装
在Ubuntu中,我们可以使用APT仓库来进行安装
-
安装Erlang,执行:
apt-get install erlang
-
执行下面的命令,新增APT仓库到
/etc/apt/sources.list.d
echo 'deb http://www.rabbitmq.com/debian/ testing main' | sudo tee /etc/apt/sources.list.d/rabbitmq.list
-
更新APT仓库的package list,执行
sudo apt-get update
命令 -
安装Rabbit Server,执行
sudo apt-get install rabbitmq-server
命令
Rabbit管理
我们可以直接通过配置文件的访问进行管理,也可以通过Web的访问进行管理。下面我们将介绍如何通过Web进行管理。
- 执行
rabbitmq-plugins enable rabbitmq_management
命令,开启Web管理插件,这样我们就可以通过浏览器来进行管理了。
> rabbitmq-plugins enable rabbitmq_management
The following plugins have been enabled:
mochiweb
webmachine
rabbitmq_web_dispatch
amqp_client
rabbitmq_management_agent
rabbitmq_management
Applying plugin configuration to rabbit@PC-201602152056... started 6 plugins.
- 打开浏览器并访问:
http://localhost:15672/
,并使用默认用户guest
登录,密码也为guest
。我们可以看到如下图的管理页面:
比如:Connections、Channels、Exchanges、Queue等。
Spring Boot整合
pom 和 配置
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring.application.name=rabbitmq-hello
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=spring
spring.rabbitmq.password=123456
创建消费者
创建消息生产者Sender
。通过注入AmqpTemplate
接口
会产生一个字符串,并发送到名为hello
的队列中。
@Component
public class Sender {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send() {
String context = "hello " + new Date();
System.out.println("Sender : " + context);
this.rabbitTemplate.convertAndSend("hello", context);
}
}
创建接受者
创建消息消费者Receiver
。通过@RabbitListener
注解定义该类对hello
队列的监听
@Component
@RabbitListener(queues = "hello")
public class Receiver {
@RabbitHandler
public void process(String hello) {
System.out.println("Receiver : " + hello);
}
}
- 创建RabbitMQ的配置类
RabbitConfig
,用来配置队列、交换器、路由等高级信息。这里我们以入门为主,先以最小化的配置来定义,以完成一个基本的生产和消费过程。
创建Mq
@Configuration
public class RabbitConfig {
@Bean
public Queue helloQueue() {
return new Queue("hello");
}
}
创建单元测试类
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = HelloApplication.class)
public class HelloApplicationTests {
@Autowired
private Sender sender;
@Test
public void hello() throws Exception {
sender.send();
}
}
启动应用主类,从控制台中,我们看到如下内容,程序创建了一个访问127.0.0.1:5672
中springcloud
的连接
Created new connection: SimpleConnection@29836d32 [delegate=amqp://springcloud@127.0.0.1:5672/]
通过RabbitMQ的控制面板,可以看到Connection和Channels中包含当前连接的条目。
- 运行单元测试类,我们可以看到控制台中输出下面的内容,消息被发送到了RabbitMQ Server的
hello
队列中。
Sender : hello Sun Sep 25 11:06:11 CST 2016
- 切换到应用主类的控制台,我们可以看到类似如下输出,消费者对
hello
队列的监听程序执行了,并输出了接受到的消息信息。
Receiver : hello Sun Sep 25 11:06:11 CST 2016
9. JavaMailSender发送邮件
引入 和 配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
spring.mail.host=smtp.qq.com
spring.mail.username=用户名
spring.mail.password=密码
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
测试指令
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Autowired
private JavaMailSender mailSender;
@Test
public void sendSimpleMail() throws Exception {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("dyc87112@qq.com");
message.setTo("dyc87112@qq.com");
message.setSubject("主题:简单邮件");
message.setText("测试邮件内容");
mailSender.send(message);
}
}
进阶使用
实际使用过程中,我们还可能会带上附件、或是使用邮件模块等。这个时候我们就需要使用MimeMessage
来设置复杂一些的邮件内容,
发送附件
在上面单元测试中加入如下测试用例(通过MimeMessageHelper来发送一封带有附件的邮件):
@Test
public void sendAttachmentsMail() throws Exception {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("dyc87112@qq.com");
helper.setTo("dyc87112@qq.com");
helper.setSubject("主题:有附件");
helper.setText("有附件的邮件");
FileSystemResource file = new FileSystemResource(new File("weixin.jpg"));
helper.addAttachment("附件-1.jpg", file);
helper.addAttachment("附件-2.jpg", file);
mailSender.send(mimeMessage);
}
嵌入静态资源
除了发送附件之外,我们在邮件内容中可能希望通过嵌入图片等静态资源,让邮件获得更好的阅读体验,而不是从附件中查看具体图片,下面的测试用例演示了如何通过MimeMessageHelper
实现在邮件正文中嵌入静态资源。
@Test
public void sendInlineMail() throws Exception {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("dyc87112@qq.com");
helper.setTo("dyc87112@qq.com");
helper.setSubject("主题:嵌入静态资源");
helper.setText("<html><body><img src=\"cid:weixin\" ></body></html>", true);
FileSystemResource file = new FileSystemResource(new File("weixin.jpg"));
helper.addInline("weixin", file);
mailSender.send(mimeMessage);
}
这里需要注意的是addInline
函数中资源名称weixin
需要与正文中cid:weixin
对应起来
模板邮件
通常我们使用邮件发送服务的时候,都会有一些固定的场景,比如重置密码、注册确认等,给每个用户发送的内容可能只有小部分是变化的。所以,很多时候我们会使用模板引擎来为各类邮件设置成模板,这样我们只需要在发送时去替换变化部分的参数即可。
在Spring Boot中使用模板引擎来实现模板化的邮件发送也是非常容易的,下面我们以velocity为例实现一下。
引入velocity模块的依赖:
在resources/templates/
下,创建一个模板页面template.vm
:
<html>
<body>
<h3>你好, ${username}, 这是一封模板邮件!</h3>
</body>
</html>
我们之前在Spring Boot中开发Web应用时,提到过在Spring Boot的自动化配置下,模板默认位于resources/templates/
目录下
最后,我们在单元测试中加入发送模板邮件的测试用例,具体如下:
@Test
public void sendTemplateMail() throws Exception {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("dyc87112@qq.com");
helper.setTo("dyc87112@qq.com");
helper.setSubject("主题:模板邮件");
Map<String, Object> model = new HashedMap();
model.put("username", "didi");
String text = VelocityEngineUtils.mergeTemplateIntoString(
velocityEngine, "template.vm", "UTF-8", model);
helper.setText(text, true);
mailSender.send(mimeMessage);
}
尝试运行一下,就可以收到内容为你好, didi, 这是一封模板邮件!
的邮件。这里,我们通过传入username的参数,在邮件内容中替换了模板中的${username}
变量。
10. 应用的后台运行配置
Spring Boot应用的几种运行方式:
- 运行Spring Boot的应用主类
- 使用Maven的Spring Boot插件
mvn spring-boot:run
来运行 - 打成jar包后,使用
java -jar
运行
Windows下比较简单,我们可以直接使用这款软件:AlwaysUp
我们只需要把Spring Boot应用通过mvn install
打成jar包,然后编写一个java -jar yourapp.jar
的bat文件。再打开AlwaysUp
,点击工具栏的第一个按钮,如下图所示,选择上面编写的bat文件,并填写服务名称。
Linux
nohup和Shell
该方法主要通过使用nohup
命令来实现
nohup 命令
用途:不挂断地运行命令。
语法:nohup Command [ Arg … ][ & ]
描述:nohup 命令运行由 Command 参数和任何相关的 Arg 参数指定的命令,忽略所有挂断(SIGHUP)信号。在注销后使用 nohup 命令运行后台中的程序。
要运行后台中的 nohup 命令,添加
&
到命令的尾部。
所以,我们只需要使用nohup java -jar yourapp.jar &
命令,就能让yourapp.jar
在后台运行了。
为了方便管理
通过Shell来编写一些用于启动应用的脚本
- 关闭应用的脚本:
stop.sh
#!/bin/bash
PID=$(ps -ef | grep yourapp.jar | grep -v grep | awk '{ print $2 }')
if [ -z "$PID" ]
then
echo Application is already stopped
else
echo kill $PID
kill $PID
fi
ps -ef | grep yourapp.jar | grep -v grep
grep -v grep 很简单 ,为了去除包含grep的进程行 ,避免影响最终数据的正确性 。
awk '{ print $2 }' 就是输出 1213,如果有多行,输出多行。
root 1213 1 0 22:45 ?
- 启动应用的脚本:
start.sh
#!/bin/bash
nohup java -jar yourapp.jar --server.port=8888 &
- 整合了关闭和启动的脚本:
run.sh
,由于会先执行关闭应用,然后再启动应用,这样不会引起端口冲突等问题,适合在持续集成系统中进行反复调用。
#!/bin/bash
echo stop application
source stop.sh
echo start application
source start.sh
系统服务
在Spring Boot的Maven插件中,还提供了构建完整可执行程序的功能,什么意思呢?就是说,我们可以不用java -jar
,而是直接运行jar来执行程序。这样我们就可以方便的将其创建成系统服务在后台运行了。主要步骤如下:
- 在
pom.xml
中添加Spring Boot的插件,并注意设置executable
配置
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
- 在完成上述配置后,使用
mvn install
进行打包,构建一个可执行的jar包 - 创建软连接到
/etc/init.d/
目录下
sudo ln -s /var/yourapp/yourapp.jar /etc/init.d/yourapp
ln -s 软链接
- 在完成软连接创建之后,我们就可以通过如下命令对
yourapp.jar
应用来控制启动、停止、重启操作了
- 失败了
/etc/init.d/yourapp start | stop | restart