秦音

个人博客


  • 首页

  • 标签

  • 分类

  • 归档

SpringBoot 多模块项目打包

发表于 2020-04-09 | 分类于 SpringBoot

1. 首次打包失败

  需要将每个模块都分别进行打包,否则在某些模块时,会报某些模块的jar不存在错误;在分别打包时,需要在子模块的pom.xml文件中加入<relativePath>../../pom.xml</relativePath>即:

1
2
3
4
5
6
<parent>
<groupId>com.aiidc.parse</groupId>
<artifactId>parse-project</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

  其中,relativePath标签中的内容指父项目的pom.xml文件的相对路径。默认值为../pom.xml(即<relativePath/>)。maven首先从当前构建项目开始查找父项目的pom文件,然后从本地仓库,最后从远程仓库。RelativePath允许你选择一个不同的位置。如果默认../pom.xml 没找到父元素的pom ,不配置relativePath指向父项目的pom则会报错

2. 检查报错模块的pom.xml中是否引入了所报错的jar

  比如在打父目录jar时,A模块报找不到com.aiidc.parse jar,那在A模块中查看是否引入了com.aiidc.parse.jar,如果没引,就引入

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>com.aiidc.parse</groupId>
<artifactId>aiidc-parse-core</artifactId>
</dependency>
</dependencies>

3. spring-boot-maven-plugin插件引发的bug

   spring-boot-maven-plugin插件的作用是:在添加了该插件之后,当运行mvn package进行打包时,会打包成一个可以直接运行的 JAR 文件,使用java -jar命令就可以直接运行。也可以在POM中,指定生成的是Jar还是War(默认是生成jar <packaging>jar</packaging>)
  在使用springboot搭建多模块项目,pom引用依赖关系正常,编译正常情况。父项目maven package出现如下异常:

3.1 第一类异常
1
2
[ERROR] 符号: 类 xxx
[ERROR] xxx.java:[7,38] 程序包xxx不存在

解决方式:检查除父目录pom以及其他非打包模块的pom文件是否存在如下插件,若有则删除或注释次插件(嵌套引用下插件造成编译异常)

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

通常仅在启动模块app工程中使用此插件(配合devtools设置热部署)

1
2
3
4
5
6
7
8
9
10
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--fork : 如果没有该项配置,这个devtools不会起作用,即应用不会restart -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>

   注意:Spring Boot中默认打包成的jar叫做可执行jar,这种jar不同于普通的jar,普通的jar不可以通过java -jar xxx.jar命令执行, 普通的jar主要是被其他应用依赖,Spring Boot打成的jar可以执行,但是不可以被其他的应用所依赖,即使强制依赖,也无法获取里边的类。但是可执行jar并不是Spring Boot独有的,Java工程本身就可以打包成可执行jar。

   既然同样是执行mvn package 命令进行项目打包,为什么 Spring Boot 项目就打成了可执行 jar ,而普通项目则打包成了不可执行 jar 呢?
   原因就是两种jar目录结构不同。这我们就不得不提 Spring Boot 项目中一个默认的插件配置 spring-boot-maven-plugin ,这个打包插件存在 5 个方面的功能(在idea右侧–>maven–>aiidc-sign–>Plugins–>spring-boot下的6个命令),从插件命令就可以看出:

1
2
3
4
5
build-info:生成项目的构建信息文件 build-info.properties
repackage:这个是默认 goal,在 mvn package 执行之后,这个命令再次打包生成可执行的 jar,同时将 mvn package 生成的 jar 重命名为 *.origin
run:这个可以用来运行 Spring Boot 应用
start:这个在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理
stop:这个在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理

   这里功能,默认情况下使用就是repackage功能,其他功能要使用,则需要开发者显式配置。而repackage功能的作用,就是在打包的时候,多做一点额外的事情:
   ①首先mvn package命令对项目进行打包,打成一个jar,这个jar就是一个普通的jar,可以被其他项目依赖,但是不可以被执行。
   ②而repackage命令,对第一步打包成的jar进行再次打包,将之打成一个可执行jar,通过将第一步打成的jar重命名为*.original文件。

   对任意一个Spring Boot项目进行打包,可以执行mvn package命令,也可以直接在IDEA中点击package。
   打包成功之后,target中的文件中有两个文件,第一个 springapplication-0.0.1-SNAPSHOT.jar 表示打包成的可执行jar,第二个 springapplication-0.0.1-SNAPSHOT.jar.original 则是在打包过程中,被重命名的jar,这是一个不可执行jar,但是可以被其他项目依赖的jar。
   其中可执行jar中,我们自己的代码是存在于BOOT-INF/classes/目录下,另外,还有一个META-INF的目录,该目录下有一个MANIFEST.MF文件,打开该文件,可以看到,这里定义了一个Start-Class,这就是可执行jar的入口类,Spring-Boot-Classes表示我们自己代码编译后的位置,Spring-Boot-Lib则表示项目依赖的jar的位置。

   换句话说,如果自己要打一个可执行jar包的话,除了添加相关依赖之外,还需要配置META-INF/MANIFEST.MF文件。而不可执行jar中(首先需要将默认的后缀.original除去,才进行解压)解压后可以看到,不可执行jar根目录就相当于我们的classpath,解压之后,直接就能看到我们的代码,它也有META-INF/MANIFEST.MF文件,但是文件中没有定义启动类等。

   注意:这个不可以执行jar也没有将项目的依赖打包进来。
   从这里我们就可以看出,两个jar,虽然都是jar包,但是内部结构是完全不同的,因此一个可以直接执行,另一个则可以被其他项目依赖。

   一般来说,Spring Boot直接打包成可执行jar就可以了,不建议将Spring Boot作为普通的jar被其他的项目所依赖。
   如果有这种需求,建议将被依赖的部分,单独抽出来做一个普通的Maven项目,然后在Spring Boot中引用这个Maven项目。
   如果非要将Spring Boot打包成一个普通jar被其他项目依赖,技术上来说,也是可以的,给spring-boot-maven-plugin插件添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>

   配置的classifier表示可执行jar的名字,配置了这个之后,在插件执行repackage命令时,就不会给mvn package所打成的jar重命名了,而打好包后的第一个jar表示可以被其他项目依赖的jar,第二个jar则表示一个可执行jar。
   相关链接地址:可执行 jar 和普通 jar 区别

3.2 第二类异常
1
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.21.0:test (default-test) on project webapp: There are test failures.

检查各个测试类是否打包跳过编译,可以使用@ignore注解(编译时跳过测试用例类)

1
2
3
4
5
6
7
8
9
10
//(import org.junit.Ignore;)
@Ignore
@RunWith(SpringRunner.class)
@SpringBootTest
public class CommonApplicationTests {
@Test
public void contextLoads() {

}
}

@Ignore注解标注在测试方法上时,该测试方法在编译打包时不会运行;
标注在测试类上时,该测试类所有方法在编译打包时都不会运行。

3.3 第三类异常
1
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.0.4.RELEASE:repackage (default) on project webapp: Execution default of goal org.springframework.boot:spring-boot-maven-plugin:2.0.4.RELEASE:repackage failed: Unable to find main class -> [Help 1]

spring-boot-maven-plugin插件会自动扫描启动类 并执行启动类main方法,所以检测使用插件工程下是否存在启动类(位置是否正确) 若没有添加启动类加@SpringBootApplication并重写main方法

1
2
3
4
5
6
@SpringBootApplication
public class WebappApplication {
public static void main(String[] args) {
SpringApplication.run(WebappApplication.class, args);
}
}

Spring的@Transactional注解控制事务有哪些不生效的场景

发表于 2020-04-08 | 分类于 Spring

1. 数据库引擎不支持事务

  这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。
  从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。

2. 没有被 Spring 管理

如下面例子所示:

1
2
3
4
5
6
7
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}

  如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

3.方法不是 public 的

以下来自 Spring 官方文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

  大概意思就是 @Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。

4. 自身调用问题

来看两个示例:

1
2
3
4
5
6
7
8
9
10
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}

  update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?
再来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void update(Order order) {
updateOrder(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateOrder(Order order) {
// update order
}
}

  这次在 update 方法上加了 @Transactional,updateOrder 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

  这两个例子的答案是:不管用!

  因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。

  这个的解决方案之一就是在的类中注入自己,用注入的对象再调用另外一个方法,这个不太优雅,另外一个可行的方案可以参考《Spring 如何在一个事务中开启另一个事务?》

5. 数据源没有配置事务管理器

1
2
3
4
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

如上面所示,当前数据源若没有配置事务管理器,那也是白搭!

6. 不支持事务

来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void update(Order order) {
updateOrder(order);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void updateOrder(Order order) {
// update order
}
}

  Propagation.NOT_SUPPORTED: 表示不以事务运行,当前若存在事务则挂起,详细的可以参考《事务隔离级别和传播机制》这篇文章。
  都主动不支持以事务方式运行了,那事务生效也是白搭!

7. 异常被吃了

这个也是出现比较多的场景:

1
2
3
4
5
6
7
8
9
10
11
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {

}
}
}

把异常吃了,然后又不抛出来,事务怎么回滚吧!

8. 异常类型错误

上面的例子再抛出一个异常:

1
2
3
4
5
6
7
8
9
10
11
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
throw new Exception("更新错误");
}
}
}

  这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:

1
2
3
@Transactional(rollbackFor = Exception.class)
这个配置仅限于 Throwable 异常类及其子类。
}

spring自带security做登录认证

发表于 2020-04-07 | 分类于 Spring
自定义security配置,替换掉security自带的登录认证:SecurityConfig.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.aiidc.parse.auth.config;

import com.aiidc.parse.auth.support.AuthConfigProperties;
import com.aiidc.parse.auth.support.CustomAuthenticationFilter;
import com.aiidc.parse.auth.support.TokenFilter;
import com.aiidc.parse.auth.service.UserAuthService;
import com.aiidc.parse.core.exception.ErrorCode;
import com.aiidc.parse.core.query.CommonResponse;
import com.aiidc.parse.core.query.SecurityConstants;
import com.aiidc.parse.core.utils.JsonUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.PrintWriter;

/**
* 自定义身份验证类(用于重写WebSecurityConfigurerAdapter默认配置)
*
* @Configuration 表示这是一个配置类
* @EnableWebSecurity 允许security
* configure() 该方法重写了父类的方法,用于添加用户与角色
* ${user.auth.switch} :用户认证开关,true:开启用户认证;false:关闭用户认证. 与启动类中的
* exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class}一起使用才有效果。
* 原理是:ConditionalOnProperty注解来觉得是否生成我们自定义的bean。而exclude来排除security的自动配置。
* 如果只有ConditionalOnProperty注解,而security会自动配置,那么就会使用security的自定义登录与认证;
* 而exclude来排除security的自动配置但在正常启动后还是可以正常运行,即它只是将security的自动配置关闭了而已
*/
@Configuration
@ConditionalOnProperty(name = "user.auth.switch", havingValue = "true")
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)// 启用方法级别的权限认证
@EnableConfigurationProperties(AuthConfigProperties.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private AuthConfigProperties authConfigProperties;

@Autowired
private TokenFilter tokenFilter;

/**
* Http安全配置
* 表单登录:使用默认的表单登录页面和登录端点/login进行登录
* 退出登录:使用默认的退出登录端点/logout退出登录
* 记住我:使用默认的“记住我”功能,把记住用户已登录的Token保存在内存里,记住30分钟
* 权限:除了指定接口之外的其它请求都要求用户已登录
* 注意:Controller中也对URL配置了权限,如果WebSecurityConfig中和Controller中都对某文化URL配置了权限,则取较小的权限
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()//支持跨域
.disable()
.authorizeRequests()
//指定接口放行
.antMatchers(buildIgnoreUrls()).permitAll()
//OPTIONS请求全部放行
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//其他接口全部接受验证
.anyRequest().authenticated();
//使用自定义的 Token过滤器 验证请求的Token是否合法
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
http.headers().cacheControl();
}

@Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try (PrintWriter writer = response.getWriter()) {
writer.write(
JsonUtil.obj2jsonString(
CommonResponse.success(authentication.getPrincipal())
)
);
}
});

filter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try (PrintWriter writer = response.getWriter()) {
writer.write(
JsonUtil.obj2jsonString(
CommonResponse.error(ErrorCode.LOGIN_AUTHORIZE_ERROR)
)
);
}
});
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}

@Bean
public TokenFilter authenticationTokenFilterBean(UserAuthService userAuthService) {
return new TokenFilter(userAuthService, SecurityConstants.HEADER_AUTH_STRING, buildIgnoreUrls());
}

private String[] buildIgnoreUrls() {
return ArrayUtils.addAll(this.authConfigProperties.getAuthIgnoreUrls(), SecurityConstants.AUTH_IGNORE_URL);
}
}
token认证与判断:TokenFilter.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package com.aiidc.parse.aiidcjusticedimension.support;

import com.aiidc.parse.aiidcjusticedimension.service.UserService;
import com.aiidc.parse.aiidcjusticedimension.service.UserTokenService;
import com.aiidc.parse.aiidcparsedao.entity.User;
import com.aiidc.parse.aiidcparsedao.entity.UserToken;
import com.aiidc.parse.core.exception.AppException;
import com.aiidc.parse.core.exception.ErrorCode;
import com.aiidc.parse.core.query.CommonResponse;
import com.aiidc.parse.core.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

/**
* token验证
*
* @author: lg
* @date: 2019-07-30 10:42
*/
@Slf4j
public class TokenFilter extends OncePerRequestFilter {

private final UserTokenService tokenService;
private final UserService userService;
private final String authHeaderName;
private final String[] authIgnorePatterns;
private final AntPathMatcher pathMatcher = new AntPathMatcher();

public TokenFilter(
UserService userService,
UserTokenService tokenService,
String authHeaderName,
String[] authIgnorePatterns) {
super();
this.userService = userService;
this.tokenService = tokenService;
this.authHeaderName = authHeaderName;
this.authIgnorePatterns = authIgnorePatterns;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = request.getHeader(authHeaderName);
if (StringUtils.isBlank(token)) {
handleAuthError(response, ErrorCode.NOT_LOGIN.errorCode(), ErrorCode.NOT_LOGIN.message());
return;
}
try {
UserToken userToken = tokenService.getByToken(token);
if (Objects.isNull(userToken)) {
throw AppException.with(ErrorCode.TOKEN_INVALID);
}
Long userId = userToken.getUserId();
User user = userService.getById(userId);
if (Objects.isNull(user)) {
throw AppException.with(ErrorCode.TOKEN_INVALID);
}

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
user, null, Arrays.asList(new SimpleGrantedAuthority("ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
//将登陆用户放到当前线程上下文中
UserContextHolder.setUser(user);
} catch (AppException e) {
handleAuthError(response, e.getErrorCode(), e.getMessage());
return;
} catch (Exception e) {
log.error("验证用户出错:token={},错误:{}", token, e);
handleAuthError(response, ErrorCode.UNAUTHORIZED.errorCode(), ErrorCode.UNAUTHORIZED.message());
return;
}
chain.doFilter(request, response);
//清除信息
log.info("clear user info........................");
UserContextHolder.remove();
}

/**
* 对鉴权异常进行统一返回
*
* @param response
* @param errorCode 错误码
* @param errorMessage 错误信息
* @throws IOException
*/
protected void handleAuthError(HttpServletResponse response,
int errorCode,
String errorMessage) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JsonUtil.obj2jsonString(CommonResponse.error(errorCode, errorMessage)));
}

/**
* 对不需要鉴权的URI直接放行
*
* @param request
* @return
* @throws ServletException
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String requestURI = request.getRequestURI();
String method = request.getMethod();
for (String ignoreURI : authIgnorePatterns) {
if (pathMatcher.match(ignoreURI, requestURI)) {
log.debug("uri={}不需要鉴权,直接放行....{}", requestURI, method);
return true;
}
}
return super.shouldNotFilter(request);
}
}
TokenUtil.class
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
package com.aiidc.parse.core.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class TokenUtil {

private TokenUtil(){
}

private static final String CLAIM_KEY_USERNAME = "username";

/**
* JWT密码
*/
private static final String SECRET = "casic";

/**
* 30分钟(毫秒)
*/
private static final long EXPIRATION_TIME = 30 * 60 * 1000;

/**
* 生成一个token
* @return
*/
public static String generateToken(){
return Jwts.builder().setId(String.valueOf(UUID.randomUUID()))
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
}

/**
* 签发JWT
*/
public static String generateToken(String username) {
Map<String, Object> claims = new HashMap<>(16);
claims.put(CLAIM_KEY_USERNAME, username);

return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(Instant.now().toEpochMilli() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}


/**
* 检查token是否过期
* @param token
* @return true=未过期,false过期
*/
public static Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}

/**
* 根据token获取username
*/
public static String getUsernameFromToken(String token) {
String username = getClaimsFromToken(token).get(CLAIM_KEY_USERNAME,String.class);
return username;
}

/**
* 获取token的过期时间
*/
public static Date getExpirationDateFromToken(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration;
}

/**
* 解析JWT
*/
private static Claims getClaimsFromToken(String token) {

Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
return claims;
}
}
放行接口读取:AuthConfigProperties.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.aiidc.parse.aiidcjusticedimension.support;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* 从配置文件读取不需要认证的接口
* <p>针对不同模块下的不同接口需求</p>
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "auth")
public class AuthConfigProperties {
private String[] authIgnoreUrls;
}
自定义的认证过滤器:CustomAuthenticationFilter.class
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
package com.aiidc.parse.aiidcjusticedimension.support;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* 自定义的认证过滤器
* <p>实现POST JSON方式登陆</p>
*
* @author lg
* @since 2020-03-02
*/
@Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
ObjectMapper mapper = new ObjectMapper();

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)
|| request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
UsernamePasswordAuthenticationToken authRequest = obtainAuthToken(request).get();
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}

return super.attemptAuthentication(request, response);
}

protected Optional<UsernamePasswordAuthenticationToken> obtainAuthToken(HttpServletRequest request) {
try (InputStream in = request.getInputStream()) {
Map<String, String> requestBody = mapper.readValue(in, Map.class);
if (Objects.nonNull(requestBody)) {
return Optional.of(
new UsernamePasswordAuthenticationToken(
requestBody.get(getUsernameParameter()),
requestBody.get(getPasswordParameter()))
);
}
} catch (IOException e) {
log.error("读取登陆的用户名和密码错误:{}", e);
}

return Optional.of(
new UsernamePasswordAuthenticationToken(StringUtils.EMPTY, StringUtils.EMPTY));
}
}
UserContextHolder.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.aiidc.parse.auth.support;

import com.aiidc.parse.aiidcparsedao.entity.User;

import java.util.Optional;

public class UserContextHolder {

private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();

public static void setUser(User user) {
USER_THREAD_LOCAL.set(user);
}

public static Optional<User> getUser() {
return Optional.ofNullable(USER_THREAD_LOCAL.get());
}

public static void remove() {
USER_THREAD_LOCAL.remove();
}
}
JsonUtil.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package com.aiidc.parse.core.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Objects;

/**
* java 基于jackson的json工具类.可以使用该类实现json字符串转java pojo,例如{@link JsonUtil#string2Obj(String, Class)},
* 反过来,也可以实现java pojo序列化为json字符串,例如{@link JsonUtil#obj2jsonString(Object)}
*
* <p>假设我们有一个{@code Artist}类</p>
* <pre>
* {@code
* class Artist {
* private String name;
* private Date birthDate;
* private LocalDate debutDate;
* //getter,setter...
* }
* }
* </pre>
* <p>
* 那么,将Artist的对象转为json字符串:
* <pre>
* {@code
* Artist art = new Artist();
* art.setName("周星驰");
* art.setBirthDate(DateUtil.toDate("1962-06-22","yyyy-MM-dd"))
* art.setDebutDate(DateUtil.toLocalDate("1980-01-01"));
*
* String jsonStr = JsonUtil.obj2jsonString(art);
*
* }
* </pre>
* 这样,我们就将一个对象转为json字符串了:
* <pre>
* {"name":"周星驰","birthDate":"1962-06-22 00:00:00","debutDate":"1980-01-01"}
* </pre>
* 这个例子中,我们也可以看出,该工具类将{@link java.util.Date}序列化的格式为{@code yyyy-MM-dd HH:mm:ss},
* 对于Java8中的{@link LocalDateTime},{@link LocalDate},{@link LocalTime}则是分别是{@code yyyy-MM-dd HH:mm:ss,yyyy-MM-dd,HH:mm:ss},
*
* <p>对于上面的json字符串,我们可以进行反序列化得到一个{@code Artist}实例</p>
* <pre>
* {@code
* Artist art1 = JsonUtil.string2Obj(jsonStr,Artist.class);
*
* System.out.println(art1.getName());//周星驰
* }
*
* </pre>
* <p>
* 工具类还提供了一个{@link JsonUtil#obj2jsonStringPretty(Object)}方法,这个方法会返回带缩进的json字符串,
* 在测试的时候很有用,输出的json格式相当美观,但是建议生产环境不要使用,因为带缩进的json相对就比较大了.
*
* <p>
* 此外,还有两个特殊的方法{@link JsonUtil#string2Obj(String, TypeReference)} 以及{@link JsonUtil#string2Obj(String, Class, Class[])}
* 这两个方法主要用于将json字符串反序列化为泛型集合时使用的,如果我们使用{@link JsonUtil#string2Obj(String, Class)}这种方式反序列化为集合类型,
* 会丢失掉类型信息.
*
* <pre>
* {@code
*
* String jsonString = [
* {"name":"许巍","birthDate":"2011-06-28 16:47:33","debutDate":"2017-08-22"},
* {"name":"张学友","birthDate":"2011-06-28 16:47:33","debutDate":"2015-03-21"},
* {"name":"华仔","birthDate":"2009-03-21 16:47:33","debutDate":"2011-03-22"}
* ]
*
* List<Artist> list1 = JsonUtil.string2Obj(jsonString, List.class);
* System.out.println(list2); //这里正常
* //list1.forEach(System.out::println); //这里就会报错了,因为实际在反序列化时存的是Map类型
*
* //对于这类反序列化,我们需要指定引用类型
* List<Artist> artistList = JsonUtil.string2Obj(jsonString,new TypeReference<List<Artist>>(){});
* list1.forEach(System.out::println); //ok
*
* //该方法等价于
* List<Artist> arts = JsonUtil.string2Obj(jsonString,List.class,Artist.class);
* arts.forEach(System.out::println); //ok
* }
*
* </pre>
* </p>
*
* @author lg
* @see ObjectMapper
*/
public class JsonUtil {

private static ObjectMapper objectMapper = new ObjectMapper();

private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);

private JsonUtil() {
}

static {
//注册java8的一些模块
objectMapper
.registerModule(new ParameterNamesModule())//构造函数,工厂方法的支持
.registerModule(new Jdk8Module()) //Optional等类的支持
.registerModule(
new JavaTimeModule()
.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateUtil.DEFAULT_DAT_TIME_FORMATTER))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateUtil.DEFAULT_DATE_FORMATTER))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateUtil.DEFAULT_TIME_FORMATTER))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateUtil.DEFAULT_DAT_TIME_FORMATTER))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateUtil.DEFAULT_DATE_FORMATTER))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateUtil.DEFAULT_TIME_FORMATTER))
); //java8时间的支持

// 全部字段序列化
//对象的所有字段全部列入
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
//取消默认转换timestamps形式
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//所有的日期格式都统一为以下的样式,即yyyy-MM-dd HH:mm:ss
objectMapper.setDateFormat(new SimpleDateFormat(DateUtil.DEFAULT_DATE_TIME_PATTERN));
//忽略空Bean转json的错误
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//map中的属性通过key值排序
objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
}

/**
* 将对象转为json string
*
* @param obj
* @return
*/
public static String obj2jsonString(Object obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
LOGGER.error("对象转json string出错,", e);
return null;
}
}

/**
* 转成有格式的json字符串,这个应该用于测试环境,生产环境最好不要用,因为会产生更大的json.
*
* @param obj
* @return
*/
public static String obj2jsonStringPretty(Object obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ?
(String) obj :
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonProcessingException e) {
LOGGER.error("对象转json string出错,", e);
return null;
}
}

/**
* 字符串转对象
*
* @param jsonStr 待解析的json字符串
* @param clazz 目标类对象
* @return
*/
public static <T> T string2Obj(String jsonStr, Class<T> clazz) {
if (StringUtils.isEmpty(jsonStr) || Objects.isNull(clazz)) {
return null;
}

try {
return clazz.equals(String.class) ? (T) jsonStr : objectMapper.readValue(jsonStr, clazz);
} catch (Exception e) {
LOGGER.error("对象转json string出错,", e);
return null;
}
}


/**
* 字段符转泛型集合
*
* <pre>
* {@code
* class Artist {
* private String name;
* private Date birthDate;
* private LocalDate debutDate;
*
* //getter,setter...
* }
* String jsonString = [
* {"name":"许巍","birthDate":"2011-06-28 16:47:33","debutDate":"2017-08-22"},
* {"name":"张学友","birthDate":"2011-06-28 16:47:33","debutDate":"2015-03-21"},
* {"name":"华仔","birthDate":"2009-03-21 16:47:33","debutDate":"2011-03-22"}
* ]
*
* List<Artist> list1 = JsonUtil.string2Obj(jsonString, List.class);
* System.out.println(list2); //这里正常
* //list1.forEach(System.out::println); //这里就会报错了,因为实际在反序列化时存的是Map类型
*
* //对于这类反序列化,我们需要指定引用类型
* List<Artist> artistList = JsonUtil.string2Obj(jsonString,new TypeReference<List<Artist>>(){});
* list1.forEach(System.out::println); //ok
*
* //该方法等价于
* List<Artist> arts = JsonUtil.string2Obj(jsonString,List.class,Artist.class);
* arts.forEach(System.out::println); //ok
* }
* </pre>
*
* @param jsonStr
* @param typeReference
* @return
* @see JsonUtil#string2Obj(String, Class, Class[])
*/
public static <T> T string2Obj(String jsonStr, TypeReference typeReference) {
if (StringUtils.isEmpty(jsonStr) || typeReference == null) {
return null;
}
try {
return (T) (typeReference.getType().equals(String.class) ? jsonStr : objectMapper.readValue(jsonStr, typeReference));
} catch (Exception e) {
LOGGER.error("json字符串反序列化失败,", e);
return null;
}
}

/**
* 与{@code string2Obj(String jsonStr,TypeReference typeReference)}效果一致
* <pre>
* {@code
* List<Artist> arts = JsonUtil.string2Obj(jsonString,List.class,Artist.class);
* arts.forEach(System.out::println); //ok
* }
* </pre>
*
* @param jsonStr 待反序列化的json字符串
* @param collectionClass 集合类,例如{@code List.class}
* @param elementClasses 元素类型,例如{@code Artist.class}
* @return
* @see JsonUtil#string2Obj(String, TypeReference)
*/
public static <T> T string2Obj(String jsonStr, Class<?> collectionClass, Class<?>... elementClasses) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
try {
return objectMapper.readValue(jsonStr, javaType);
} catch (Exception e) {
LOGGER.error("json字符串反序列化失败,", e);
return null;
}
}

public static int getValueAsInt(String jsonStr, String key) throws IOException {
try {
JsonNode jsonNode = objectMapper.readTree(jsonStr);
return jsonNode.get(key).asInt();
} catch (IOException e) {
throw e;
}
}

public static String getValueAsText(String jsonStr, String key) throws IOException {
try {
JsonNode jsonNode = objectMapper.readTree(jsonStr);
return jsonNode.get(key).asText();
} catch (IOException e) {
throw e;
}
}
}
Swagger2Config.class
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
package com.aiidc.parse.core.config;

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.context.request.async.DeferredResult;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

@Profile("!production")
@Configuration
@EnableSwagger2
@Slf4j
public class Swagger2Config {

@Bean
public Docket restfulApi() {
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
tokenPar.name("X-Request-Token").description("令牌")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false).build();
pars.add(tokenPar.build());

return new Docket(DocumentationType.SWAGGER_2)
.groupName("航天智慧-解析服务")
.genericModelSubstitutes(DeferredResult.class)
.useDefaultResponseMessages(true)
.forCodeGeneration(true)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.build()
.globalOperationParameters(pars)
.apiInfo(intoApiInfo());
}

private ApiInfo intoApiInfo() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("航天智慧-解析服务")
.description("(航天智慧-解析服务)")
.version("1.0")
.build();
return apiInfo;
}
}
相关jar与启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<jjwt.version>0.9.1</jjwt.version>

<!--使用spring自带的security做的登录认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--用于token生成与签发JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
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
package com.aiidc.parse.aiidcjusticedimension;

import com.hankcs.hanlp.utility.Predefine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
@ComponentScan("com.aiidc.parse.*")
public class AiidcJusticeDimensionApplication {

/**
* 启动时可配置外部hanlp.properties
* -Dhanlp_properties_path=
*/
public static void main(String[] args) {
//指定hanlp的配置文件路径
String hanlp_properties_path = System.getProperty("hanlp_properties_path");
System.out.println("hanlp_properties_path:" + hanlp_properties_path);
//读取设置的hanlp.properties路径
if (hanlp_properties_path != null) {
Predefine.HANLP_PROPERTIES_PATH = hanlp_properties_path;
}
SpringApplication.run(AiidcJusticeDimensionApplication.class, args);
}
}

Hexo博客搭建流程与注意事项

发表于 2020-04-03 | 分类于 博客搭建

搭建流程

在GitHub上创建项目的名称必须与你的github用户名相同,比如用户名为qinyin-yq,系统名必须为:qinyin-yq.github.io,即《系统名.github.io》

安装Hexo:npm install -g hexo-cli
安装 Hexo 完成后,再执行下列命令,Hexo 将会在指定文件夹中新建所需要的文件。

1
2
3
hexo init myBlog
cd myBlog
npm install

新建完成后,指定文件夹的目录如下:
├── _config.yml # 网站的配置信息,您可以在此配置大部分的参数。
├── package.json
├── scaffolds # 模版文件夹
├── source # 资源文件夹,除 _posts 文件,其他以下划线_开头的文件或者文件夹不会被编译打包到public文件夹
| ├── _drafts # 草稿文件
| └── _posts # 文章Markdowm文件
└── themes # 主题文件夹

之后将Hexo与github两个关联起来:

  1. 打开项目根目录下的 _config.yml 配置文件配置参数。拉到文件末尾,填上如下配置(也可同时部署到多个仓库,后面再说):

    1
    2
    3
    4
    5
    deploy:
    type: git
    repo:
    github: https://github.com/qinyin-yq/qinyin-yq.github.io.git
    branch: master
  2. 要安装一个部署插件 hexo-deployer-git:

    npm install hexo-deployer-git --save
  3. 在本地修改项目后,要更新到github Page上时,应该现在本地测试看效果:

    hexo server (或 hexo s)

    然后部署上线:g 是 generate 缩写,d 是 deploy 缩写

    hexo clean    //清除缓存文件 (db.json) 和已生成的静态文件 (public)
    hexo g    //(本地生成静态文件)
    hexo d     //(将本地静态文件推送至Github)
  4. draft是草稿的意思,也就是你如果想写文章,又不希望被看到,那么可以:hexo new draft newpage。这样会在source/_draft中新建一个newpage.md文件,如果你的草稿文件写的过程中,想要预览一下,那么可以使用:hexo server --draft。然后在本地端口中开启服务预览。
    如果你的草稿文件写完了,想要发表到post中:hexo publish draft newpage就会自动把newpage.md发送到post中。

还有另外一种方式:

直接在github上新建项目qinyin,然后在项目上创建html页面,比如index.html页面,然后在setting>GitHub Pages>Source>选择需要关联的分支,然后就 https://qinyin-yq.github.io/qinyin(项目名)/index.html(html页面名) 可以访问了,然后通过index.html上的页面按钮跳转到其他页面(这种方式没有深入实践不清楚可行性)

样式修改

  • next的主题修改(比如头部Home的地址,图标等)在..\themes\next\_config.yml文件中menu属性修改, 而底部是否显示主题样式、地址与版本号,则是footer:属性中修改,而博客的更新时间是updated_at这个属性决定。
  • 首页的内容是在..\source\_posts\hello-world.md文件中修改
  • 博客中的英文转中文,比如:Posted on是根据在..\_config.yml文件中language属性指定何种语言格式来确定的。英文对应于en,中文对应zh-Hans。包括其他例如“首页”“标签”等同理。
  • 菜单与侧边栏的英文替换中文是在对应的.yml文件中修改(中文对应的是zh-Hans.yml)而指定使用哪个.yml文件是在..\_config.yml文件中language属性指定(language: zh-Hans)
  • 侧边栏目录添加序号问题:在新增博客内容时,自己在内容中有编号,但next默认又会给我们添加一次编号,导致编号重复。
      解决方法:关闭next自带的添加编号功能:在..\themes\next\_config.yml文件中toc.number属性设置
  • 头像设置:在..\themes\next\_config.yml文件中avatar属性修改,指定头像图片的路径,相对路径是在当前主题,即..\themes\next\source目录下。也可以使用绝对路径
  • 修改”发表于2020-04-08|分类于 Spring”下的margin-bottom过大的样式:..\themes\next\source\css\_common\components\post\post-meta.styl文件中.posts-expand .post-meta{}属性
  • 修改h1-7 标题中的间距问题:..\themes\next\source\css\_common\components\post\post-expand.styl文件中h2, h3, h4, h5, h6{}属性删除了:padding-top: 10px;
  • 修改代码块<figure>标签上下间距:..\themes\next\source\css\_common\components\highlight\highlight.styl文件code-block属性:margin: 10px 0;
  • 修改博客与博客之间的间距过大,即横线的上下间距过大的样式:..\themes\next\source\css\_variables\base.styl中的:
    1
    2
    $post-eof-margin-top          = 80px  //  or 160px for more white space
    $post-eof-margin-bottom = 60px // or 120px for less white space
  • 修改图片与文字之间的间隔过大:..\themes\next\source\css\_common\components\post\post-expand.styl文件
    1
    .posts-expand .post-body .fancybox img { margin: 0 auto 5px; }
  • 修改

    标签的样式..\themes\next\source\css\_common\scaffolding\base.styl文件

    1
    p { margin: 0 0 0 0; }

标签类别管理

  • 创建标签
    创建标签页面:hexo new page tags
  • 创建分类
    创建分类页面:hexo new page categories

博客内容引入图片

将图片放在..\themes\next\source\images目录下,然后在博客的内容中,通过

1
![](/images/xxxx.jpg)

格式引入本地图片。

相关链接

Markdown在线编辑器
hexo史上最全搭建教程
GitHub——GitHub Pages+Hexo 搭建独立博客的优化
hexo + github pages搭建博客样式加载不出来+url的配置问题
仓库主页

Hello World

发表于 2017-12-02

图片开始图片结束

s

Linux安装链接:使用VMware新建Red Hat Enterprse Linux 6操作系统

  

内容折叠标题

折叠内容

<123
YouQiang

YouQiang

技术总结

25 日志
18 分类
35 标签
GitHub
© 2020 YouQiang