前言
这篇文章我们来分析一下org.springframework.boot.actuate.security,org.springframework.boot.actuate.audit中的代码,这2个包的类是对spring security 的事件进行处理的.类图如下:

二、源码解析

2.1、AuditEvent事件类

AuditEvent–> 1个值对象–>代表了1个audit event: 在特定的时间,1个特定的用户或者代理,实施了1个特定类型的动作.AuditEvent记录了有关AuditEvent的细节.

其类上有如下注解:

@JsonInclude(Include.NON_EMPTY)

代表该类中为空(“”)或者为null的属性不会被序列化。

该类的字段如下:

private final Date timestamp;

// 资源
private final String principal; private final String type; private final Map<String, Object> data;

2.2、AuditApplicationEvent事件类

AuditApplicationEvent–> 封装AuditEvent.代码如下:

public class AuditApplicationEvent extends ApplicationEvent {

    private final AuditEvent auditEvent;

    public AuditApplicationEvent(String principal, String type,
Map<String, Object> data) {
this(new AuditEvent(principal, type, data));
} AuditApplicationEvent(String principal, String type, String... data) {
this(new AuditEvent(principal, type, data));
} public AuditApplicationEvent(Date timestamp, String principal, String type,
Map<String, Object> data) {
this(new AuditEvent(timestamp, principal, type, data));
} public AuditApplicationEvent(AuditEvent auditEvent) {
super(auditEvent);
Assert.notNull(auditEvent, "AuditEvent must not be null");
this.auditEvent = auditEvent;
} public AuditEvent getAuditEvent() {
return this.auditEvent;
} }

2.3、AbstractAuditListener

AbstractAuditListener –>处理AuditApplicationEvent事件的抽象类.代码如下:

public abstract class AbstractAuditListener
implements ApplicationListener<AuditApplicationEvent> { @Override
public void onApplicationEvent(AuditApplicationEvent event) {
onAuditEvent(event.getAuditEvent());
} protected abstract void onAuditEvent(AuditEvent event); }

2.4、AuditEventRepository

AuditEventRepository–> 关于AuditEvent的dao实现.声明了如下4个方法:

// 添加日志
void add(AuditEvent event); // 查询指定日期之后的AuditEvent
List<AuditEvent> find(Date after); // 根据给定的Date和principal(资源)获得对应的AuditEvent
List<AuditEvent> find(String principal, Date after); // 根据给的date,principal,type 类获取给定的AuditEvent
List<AuditEvent> find(String principal, Date after, String type);

2.5、InMemoryAuditEventRepository

InMemoryAuditEventRepository –> AuditEventRepository接口的唯一实现.

该类的字段如下:

// AuditEvent数组默认的默认大小
private static final int DEFAULT_CAPACITY = 4000; // 用于对events进行操作时 加的锁
private final Object monitor = new Object(); /**
* Circular buffer of the event with tail pointing to the last element.
* 循环数组
*/
private AuditEvent[] events; // 最后1个元素的下标
private volatile int tail = -1;

构造器如下:

public InMemoryAuditEventRepository() {
this(DEFAULT_CAPACITY);
} public InMemoryAuditEventRepository(int capacity) {
this.events = new AuditEvent[capacity];
}

AuditEventRepository中的方法实现如下:

    @Override
public void add(AuditEvent event) {
Assert.notNull(event, "AuditEvent must not be null");
synchronized (this.monitor) {
this.tail = (this.tail + 1) % this.events.length;
this.events[this.tail] = event;
}
} @Override
public List<AuditEvent> find(Date after) {
return find(null, after, null);
} @Override
public List<AuditEvent> find(String principal, Date after) {
return find(principal, after, null);
}
//上面两个方法最终调用这个方法
@Override
public List<AuditEvent> find(String principal, Date after, String type) {
LinkedList<AuditEvent> events = new LinkedList<AuditEvent>();
synchronized (this.monitor) {
// 1. 遍历events
for (int i = 0; i < this.events.length; i++) {
// 1.1 获得最新的AuditEvent
AuditEvent event = resolveTailEvent(i);
// 1.2 如果AuditEvent 不等于null并且符合查询要求的话,就加入到events中
if (event != null && isMatch(principal, after, type, event)) {
events.addFirst(event);
}
}
}
// 2. 返回结果集
return events;
} //过滤不和条件的事件
private boolean isMatch(String principal, Date after, String type, AuditEvent event) {
boolean match = true;
match = match && (principal == null || event.getPrincipal().equals(principal));
match = match && (after == null || event.getTimestamp().compareTo(after) >= 0);
match = match && (type == null || event.getType().equals(type));
return match;
} //获得最新的AuditEvent
private AuditEvent resolveTailEvent(int offset) {
int index = ((this.tail + this.events.length - offset) % this.events.length);
return this.events[index];
}

返回结果集

这里有2个问题:

  1、前面说过访问events的时候都需要进行加锁,为什么resolveTailEvent方法没有加锁?

    原因如下: resolveTailEvent的调用点只有1个,就是在find(String Date , String)中,而在该方法中已经加锁了,因此该方法不需要加锁.

  2、resolveTailEvent方法加锁可以吗

    答: 可以,原因是synchronized 是可重入的.但是不推荐,如果加上,会产生性能损耗.

关于这个方法的实现原理我们还是举个例子比较好.假设我们的数组长度为3个,此时已经放满数组了,如下:

[0,1,2]

此时tail = 2, 然后我们继续放入3,则数组如下:

[3,1,2],此时tail = 0. 然后我们调用find.在该方法中会调用resolveTailEvent.

第1次传入的是0,则index = (0+3-0)%3 = 0,获得的正是3.
第2次传入的是1,则index = (0+3-1)%3 = 2,获得的正是2.
第3次传入的是2,则index = (0+3-2)%3 = 1,获得的正是1.
因此说find(String, Date, String)获得的结果时按照添加的顺序倒序返回的.

自动装配:

声明在AuditAutoConfiguration类内的static AuditEventRepositoryConfiguration配置类中,代码如下:

@ConditionalOnMissingBean(AuditEventRepository.class)
protected static class AuditEventRepositoryConfiguration { @Bean
public InMemoryAuditEventRepository auditEventRepository() throws Exception {
return new InMemoryAuditEventRepository();
} }

当beanFactory中不存在 AuditEventRepository类型的bean时生效.注册1个id为auditEventRepository,类型为InMemoryAuditEventRepository的bean.

2.6、AuditListener

AuditListener–> AbstractAuditListener的默认实现.监听AuditApplicationEvent事件然后存储到AuditEventRepository中. 代码如下:

public class AuditListener extends AbstractAuditListener {

    private static final Log logger = LogFactory.getLog(AuditListener.class);

    private final AuditEventRepository auditEventRepository;

    public AuditListener(AuditEventRepository auditEventRepository) {
this.auditEventRepository = auditEventRepository;
} @Override
protected void onAuditEvent(AuditEvent event) {
if (logger.isDebugEnabled()) {
logger.debug(event);
}
this.auditEventRepository.add(event);
} }

监听到AuditApplicationEvent时,直接将其封装的AuditEvent加入到AuditEventRepository中.还是比较简单的.

自动装配如下:

在AuditAutoConfiguration中进行了声明,代码如下:

    @Bean
@ConditionalOnMissingBean(AbstractAuditListener.class)
public AuditListener auditListener() throws Exception {
return new AuditListener(this.auditEventRepository);
}

@Bean–> 注册1个id为auditListener,类型为AuditListener的bean
@ConditionalOnMissingBean(AbstractAuditListener.class) –> 当beanFactory中不存在类型为AbstractAuditListener的bean时生效。

注意,在AuditListener中注入的是InMemoryAuditEventRepository

2.7AbstractAuthenticationAuditListener

AbstractAuthenticationAuditListener–> 暴露 Spring Security AbstractAuthenticationEvent(认证事件) 将其转换为AuditEvent 的抽象ApplicationListener基类.

代码如下:

public abstract class AbstractAuthenticationAuditListener implements
ApplicationListener<AbstractAuthenticationEvent>, ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; @Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
} protected ApplicationEventPublisher getPublisher() {
return this.publisher;
} protected void publish(AuditEvent event) {
if (getPublisher() != null) {
getPublisher().publishEvent(new AuditApplicationEvent(event));
}
}
}

2.8、AuthenticationAuditListener

AuthenticationAuditListener的默认实现
字段如下:

// 当发生AuthenticationSuccessEvent事件时添加到AuditEvent中的type
public static final String AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"; // 当发生AbstractAuthenticationFailureEvent事件时添加到AuditEvent中的type
public static final String AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE"; // 当发生AuthenticationSwitchUserEvent事件时添加到AuditEvent中的type
public static final String AUTHENTICATION_SWITCH = "AUTHENTICATION_SWITCH"; private static final String WEB_LISTENER_CHECK_CLASS = "org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent"; private WebAuditListener webListener = maybeCreateWebListener(); // 只要加入spring-boot-starter-security的依赖,就会在当前类路径下存在org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent
// 因此会返回WebAuditListener
private static WebAuditListener maybeCreateWebListener() {
if (ClassUtils.isPresent(WEB_LISTENER_CHECK_CLASS, null)) {
return new WebAuditListener();
}
return null;
}

onApplicationEvent 方法实现如下:

public void onApplicationEvent(AbstractAuthenticationEvent event) {
// 1. 如果验证失败,
if (event instanceof AbstractAuthenticationFailureEvent) {
onAuthenticationFailureEvent((AbstractAuthenticationFailureEvent) event);
}
// 2.如果webListener不等于null.并且该事件为AuthenticationSwitchUserEvent
else if (this.webListener != null && this.webListener.accepts(event)) {
this.webListener.process(this, event);
}
// 3. 如果是AuthenticationSuccessEvent
else if (event instanceof AuthenticationSuccessEvent) {
onAuthenticationSuccessEvent((AuthenticationSuccessEvent) event);
}
}

  1、如果验证失败(AbstractAuthenticationFailureEvent),则发送AuditEvent事件,其type为AUTHENTICATION_FAILURE.代码如下:

private void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) {
Map<String, Object> data = new HashMap<String, Object>();
data.put("type", event.getException().getClass().getName());
data.put("message", event.getException().getMessage());
if (event.getAuthentication().getDetails() != null) {
data.put("details", event.getAuthentication().getDetails());
}
publish(new AuditEvent(event.getAuthentication().getName(),
AUTHENTICATION_FAILURE, data));
}

  2、如果webListener不等于null.并且该事件为AuthenticationSwitchUserEvent,则发送AuditEvent事件,其type为AUTHENTICATION_SWITCH.代码如下:

public void process(AuthenticationAuditListener listener,
AbstractAuthenticationEvent input) {
if (listener != null) {
AuthenticationSwitchUserEvent event = (AuthenticationSwitchUserEvent) input;
Map<String, Object> data = new HashMap<String, Object>();
if (event.getAuthentication().getDetails() != null) {
data.put("details", event.getAuthentication().getDetails());
}
data.put("target", event.getTargetUser().getUsername());
listener.publish(new AuditEvent(event.getAuthentication().getName(),
AUTHENTICATION_SWITCH, data));
} }

  3、如果是AuthenticationSuccessEvent,则发送AuditEvent事件,其type为AUTHENTICATION_SUCCESS.代码如下:

private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
Map<String, Object> data = new HashMap<String, Object>();
if (event.getAuthentication().getDetails() != null) {
data.put("details", event.getAuthentication().getDetails());
}
publish(new AuditEvent(event.getAuthentication().getName(),
AUTHENTICATION_SUCCESS, data));
}

自动装配:

在AuditAutoConfiguration中进行了声明,代码如下:

@Bean
@ConditionalOnClass(name = "org.springframework.security.authentication.event.AbstractAuthenticationEvent")
@ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)
public AuthenticationAuditListener authenticationAuditListener() throws Exception {
return new AuthenticationAuditListener();
}

  1、@Bean –> 注册1个id为authenticationAuditListener, AuthenticationAuditListener的bean

  2、@ConditionalOnClass(name = “org.springframework.security.authentication.event.AbstractAuthenticationEvent”)–> 当在当前类路径下存在org.springframework.security.authentication.event.AbstractAuthenticationEvent时生效
  3、@ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)–>beanFactory中不存在AbstractAuthenticationAuditListener类型的bean时生效.

2.9、AbstractAuthorizationAuditListener

AbstractAuthorizationAuditListener –>1个暴露AbstractAuthorizationEvent(授权事件)作为AuditEvent的抽象ApplicationListener基类.代码如下:

public abstract class AbstractAuthorizationAuditListener implements
ApplicationListener<AbstractAuthorizationEvent>, ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; @Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
} protected ApplicationEventPublisher getPublisher() {
return this.publisher;
} protected void publish(AuditEvent event) {
if (getPublisher() != null) {
getPublisher().publishEvent(new AuditApplicationEvent(event));
}
} }

2.10、AuthorizationAuditListener

AuthorizationAuditListener–> AbstractAuthorizationAuditListener的默认实现
字段如下:

// 发生AuthorizationFailureEvent事件时对应的AuditEvent的类型
public static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE";

onApplicationEvent代码如下:

public void onApplicationEvent(AbstractAuthorizationEvent event) {
// 1. 如果是AuthenticationCredentialsNotFoundEvent事件,则发送AuditEvent事件,type为AUTHENTICATION_FAILURE
if (event instanceof AuthenticationCredentialsNotFoundEvent) {
onAuthenticationCredentialsNotFoundEvent(
(AuthenticationCredentialsNotFoundEvent) event);
}
// 2. 如果是AuthorizationFailureEvent事件,则发送AuditEvent事件,type为AUTHORIZATION_FAILURE
else if (event instanceof AuthorizationFailureEvent) {
onAuthorizationFailureEvent((AuthorizationFailureEvent) event);
}
}

  1、如果是AuthenticationCredentialsNotFoundEvent事件,则发送AuditEvent事件,type为AUTHENTICATION_FAILURE.代码如下:

private void onAuthenticationCredentialsNotFoundEvent(
AuthenticationCredentialsNotFoundEvent event) {
Map<String, Object> data = new HashMap<String, Object>();
data.put("type", event.getCredentialsNotFoundException().getClass().getName());
data.put("message", event.getCredentialsNotFoundException().getMessage());
publish(new AuditEvent("<unknown>",
AuthenticationAuditListener.AUTHENTICATION_FAILURE, data));
}

  2、如果是AuthorizationFailureEvent事件,则发送AuditEvent事件,type为AUTHORIZATION_FAILURE.代码如下:

private void onAuthorizationFailureEvent(AuthorizationFailureEvent event) {
Map<String, Object> data = new HashMap<String, Object>();
data.put("type", event.getAccessDeniedException().getClass().getName());
data.put("message", event.getAccessDeniedException().getMessage());
if (event.getAuthentication().getDetails() != null) {
data.put("details", event.getAuthentication().getDetails());
}
publish(new AuditEvent(event.getAuthentication().getName(), AUTHORIZATION_FAILURE,
data));
}

自动装配:

在AuditAutoConfiguration中进行了装配,代码如下:

@Bean
@ConditionalOnClass(name = "org.springframework.security.access.event.AbstractAuthorizationEvent")
@ConditionalOnMissingBean(AbstractAuthorizationAuditListener.class)
public AuthorizationAuditListener authorizationAuditListener() throws Exception {
return new AuthorizationAuditListener();
}
  1. @Bean –> 注册1个id为authorizationAuditListener,类型为AuthorizationAuditListener的bean
  2. @ConditionalOnClass(name = “org.springframework.security.access.event.AbstractAuthorizationEvent”)–> 在当前类路径下存在org.springframework.security.access.event.AbstractAuthorizationEvent时生效
  3. @ConditionalOnMissingBean(AbstractAuthorizationAuditListener.class)–> beanFactory中不存在AbstractAuthorizationAuditListener类型的bean时生效.

三、流程分析

准备工作
如果想让 spring boot 应用激活AuditEvent的事件的处理,需要加入spring-boot-starter-security依赖,代码如下:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

光加入依赖还不够,我们需要加入security的配置,不然AuthorizationAuditListener,AuthenticationAuditListener 监听什么事件呢? 因此,我们加入如下代码:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/error-log").hasAuthority("ROLE_TEST").antMatchers("/", "/home")
.permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().and()
.logout().logoutUrl("/logout").permitAll().and().authorizeRequests(); } @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}

在configureGlobal中,我们在内存中生成了1个用户:用户名为user,密码为password,角色为USER.
在configure中我们配置了如下内容:

  1. 访问/error-log需要拥有ROLE_TEST的权限
  2. 访问/,/home不需要进行验证
  3. 登录页面为/login,不需要进行验证
  4. 登出页面为/logout
  5. 其他链接都需要进行验证

声明1个UserController,代码如下:

@Controller
public class UserController { @RequestMapping("/")
public String index() {
return "index";
} @RequestMapping("/hello")
public String hello() {
return "hello";
} @RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "login";
} @RequestMapping("/error-test")
public String error() {
return "1";
}
}

在src/main/resources/templates目录下创建如下几个页面:

hello.html,代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="注销"/>
</form>
</body>
</html>

index.html,代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security入门</title>
</head>
<body>
<h1>欢迎使用Spring Security!</h1>
<p>点击 <a th:href="@{/hello}">这里</a> 打个招呼吧</p>
</body>
</html>

login.html,代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
用户名或密码错
</div>
<div th:if="${param.logout}">
您已注销成功
</div>
<form th:action="@{/login}" method="post">
<div><label> 用户名 : <input type="text" name="username"/> </label></div>
<div><label> 密 码 : <input type="password" name="password"/> </label></div>
<div><input type="submit" value="登录"/></div>
</form>
</body>
</html>

测试

启动应用后我们访问如下链接: http://127.0.0.1:8080/,返回的是如下页面:

点击index.html 中的超链接后,由于需要进行验证,返回到login页面,如图:

此时我们输入错误的用户名,密码,返回的页面如下:

此时我们输入user,password 后,返回的页面如下:

点击注销后,页面如下:

此时我们访问 http://127.0.0.1:8080/error-test,由于没有登录,还是调回到登录页面.

访问 http://127.0.0.1:8080/auditevents,返回的结果如下:

{
events: [
{
timestamp: "2018-01-23T03:52:13+0000",
principal: "anonymousUser",
type: "AUTHORIZATION_FAILURE",
data: {
details: {
remoteAddress: "127.0.0.1",
sessionId: null
},
type: "org.springframework.security.access.AccessDeniedException",
message: "Access is denied"
}
},
{
timestamp: "2018-01-23T03:54:21+0000",
principal: "aaa",
type: "AUTHENTICATION_FAILURE",
data: {
details: {
remoteAddress: "127.0.0.1",
sessionId: "DFDB023AEEF41BBD8079EC32402CBFD8"
},
type: "org.springframework.security.authentication.BadCredentialsException",
message: "Bad credentials"
}
},
{
timestamp: "2018-01-23T03:55:50+0000",
principal: "user",
type: "AUTHENTICATION_SUCCESS",
data: {
details: {
remoteAddress: "127.0.0.1",
sessionId: "DFDB023AEEF41BBD8079EC32402CBFD8"
}
}
},
{
timestamp: "2018-01-23T03:58:38+0000",
principal: "anonymousUser",
type: "AUTHORIZATION_FAILURE",
data: {
details: {
remoteAddress: "127.0.0.1",
sessionId: "6E6E614D638B6F5EE5B7E8CF516E2534"
},
type: "org.springframework.security.access.AccessDeniedException",
message: "Access is denied"
}
},
{
timestamp: "2018-01-23T04:00:01+0000",
principal: "anonymousUser",
type: "AUTHORIZATION_FAILURE",
data: {
details: {
remoteAddress: "127.0.0.1",
sessionId: "6E6E614D638B6F5EE5B7E8CF516E2534"
},
type: "org.springframework.security.access.AccessDeniedException",
message: "Access is denied"
}
},
{
timestamp: "2018-01-23T04:00:12+0000",
principal: "user",
type: "AUTHENTICATION_SUCCESS",
data: {
details: {
remoteAddress: "127.0.0.1",
sessionId: "6E6E614D638B6F5EE5B7E8CF516E2534"
}
}
}
]
}

解析

  1. 当我们访问 http://127.0.0.1:8080/hello.html 时,由于需要验证,因此会发送AuthorizationFailureEvent事件,此时会交由AuthorizationAuditListener处理,调用onAuthorizationFailureEvent方法.发送AuditEvent事件
  2. AuditListener 监听该事件,最终执行onAuditEvent方法,将AuditEvent存入到InMemoryAuditEventRepository中.
  3. 此时在login.html中,我们输入错误的用户名,密码,此时会发送AuthenticationFailureBadCredentialsEvent事件,交由AuthenticationAuditListener处理,最终执行onAuthenticationFailureEvent方法,发送AuditEvent事件, 最终还是在AuditListener中将事件存到 InMemoryAuditEventRepository中
  4. 此时我们输入正确的用户名密码后,会发送AuthenticationSuccessEvent事件,交由AuthenticationAuditListener事件处理,最终执行onAuthenticationSuccessEvent方法,发送AuditEvent事件,最终还是在AuditListener中将事件存到InMemoryAuditEventRepository中
  5. 之后产生了SessionFixationProtectionEvent事件,AuthenticationAuditListener不进行处理
  6. 产生InteractiveAuthenticationSuccessEvent,AuthenticationAuditListener不进行处理.
  7. 此时跳转到hello.html ,我们点击注销后,没有产生任何的事件.
  8. 登录成功后,我们访问/error-log,由于需要ROLE_TEST权限,而user 只有ROLE_USER 权限,因此会产生AuthorizationFailureEvent事件,因此会交由AuthorizationAuditListener处理,发送AuditEvent事件,最终还是在AuditListener中将事件存到InMemoryAuditEventRepository中

zhuan:https://blog.csdn.net/qq_26000415/article/details/79138270

Springboot Actuator之十:actuator中的audit包的更多相关文章

  1. SpringBoot整合Swagger和Actuator

    前言 本篇文章主要介绍的是SpringBoot整合Swagger(API文档生成框架)和SpringBoot整合Actuator(项目监控)使用教程. SpringBoot整合Swagger 说明:如 ...

  2. Springboot Actuator之八:actuator的执行原理

    本文接着<Springboot Actuator之七:actuator 中原生endpoint源码解析1>,前面主要分析了原生endpoint的作用. 现在着重了解actuator的执行原 ...

  3. SpringBoot:三十五道SpringBoot面试题及答案

    SpringBoot面试前言今天博主将为大家分享三十五道SpringBoot面试题及答案,不喜勿喷,如有异议欢迎讨论! Spring Boot 是微服务中最好的 Java 框架. 我们建议你能够成为一 ...

  4. SpringBoot开发二十-Redis入门以及Spring整合Redis

    安装 Redis,熟悉 Redis 的命令以及整合Redis,在Spring 中使用Redis. 代码实现 Redis 内置了 16 个库,索引是 0-15 ,默认选择第 0 个 Redis 的常用命 ...

  5. SpringBoot系列(十)优雅的处理统一异常处理与统一结果返回

    SpringBoot系列(十)统一异常处理与统一结果返回 往期推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列 ...

  6. SpringBoot系列(十四)集成邮件发送服务及邮件发送的几种方式

    往期推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列(三)配置文件详解 SpringBoot系列(四)web静 ...

  7. SpringBoot应用部署到Tomcat中无法启动问题

    SpringBoot应用部署到Tomcat中无法启动问题   背景 最近公司在做一些内部的小型Web应用时, 为了提高开发效率决定使用SpringBoot, 这货自带Servlet容器, 你在开发We ...

  8. SpringBoot | 第三十二章:事件的发布和监听

    前言 今天去官网查看spring boot资料时,在特性中看见了系统的事件及监听章节.想想,spring的事件应该是在3.x版本就发布的功能了,并越来越完善,其为bean和bean之间的消息通信提供了 ...

  9. docker 运行jenkins及vue项目与springboot项目(五.jenkins打包springboot服务且在docker中运行)

    docker 运行jenkins及vue项目与springboot项目: 一.安装docker 二.docker运行jenkins为自动打包运行做准备 三.jenkins的使用及自动打包vue项目 四 ...

随机推荐

  1. iis url 重写

    1.选择网站-找到有测url 重写 :2:选中它,在右上角有一个打开功能,点击打开 3.依然在右上角,点击添加规则 4:选择第一个,空白规则 名称随便输入,我们通常有这样一个需求,就是.aspx 后缀 ...

  2. c# 编码风格

    此内容为copy别人的,仅供自己参看.如有意见,麻烦通知我,谢谢 1. C# 代码风格要求 1.1注释 类型.属性.事件.方法.方法参数,根据需要添加注释. 如果类型.属性.事件.方法.方法参数的名称 ...

  3. windows10风格 springboot vue.js html 跨域 前后分离 activiti 整合项目框架源码

    官网:www.fhadmin.org 此项目为Springboot工作流版本 windows 风格,浏览器访问操作使用,非桌面应用程序. 1.代码生成器: [正反双向](单表.主表.明细表.树形表,快 ...

  4. vim 如何复制文件中多行到另一个文件

    1.打开文件 vim a.txt b.tx 或者 vim *.txt 2.文件间切换 :n 切换到下一个文件 :wn 保存再切换 :N 到上一个文件 :wN 保存再切换 :.= 看当前行 3.假定当前 ...

  5. html、css/bootStrap、js/Jquery、ajax与json数据交互总结

    设计个个人网站,利用CSS.JavaScript.HTML5.jQuery库和AJAX等 实现网站各功能模块,下面介绍我设计的网站各大功能模块:  个人简历模块: 包涵个人基本信息(利用CSS的flo ...

  6. pymysql操作(老版本的,新版有基础不同)

    导入库 import pymysql 创建链接 conn=pymysql.connect(host='127.0.0.1',port='3306',user='root',passwd='123456 ...

  7. 'root'@'localhost'不能登录问题

    鉴于我在遇到这个问题时,在网上搜索到几十个答案都是进行更改密码解决该问题,然并没有卵用.最后还是让小编找到了解决方法,希望大家遇到该问题时能够节省时间.解决方法如下:   #mysql -u root ...

  8. libpcap工具包使用go交叉编译开发android

    命令使用 libpcap交叉编译 cd /tmpwget http://www.tcpdump.org/release/libpcap-1.8.1.tar.gztar xvf libpcap-1.8. ...

  9. 20180610模拟赛T1——脱离地牢

    Description 在一个神秘的国度里,年轻的王子Paris与美丽的公主Helen在一起过着幸福的生活.他们都随身带有一块带磁性的阴阳魔法石,身居地狱的魔王Satan早就想着得到这两块石头了,只要 ...

  10. opencart按重量配送如何设置

    OpenCart中基于重量的配送模块让你使用自己的价格折算表来决定配送价格.可以基于不同的重量允许设置价格范围,重量和价格的格式如下:weight(重量1):cost(价格1), weight(重量2 ...