JoyLau's Blog

JoyLau 的技术学习与思考

GarageBand,这个是系统上的模拟乐器,一般都使用不到

1
2
3
rm -rf /Library/Application\ Support/GarageBand
rm -rf /Library/Application\ Support/Logic
rm -rf /Library/Audio/Apple\ Loops

但是有些系统文件显示占用的空间很大,该怎么看呢

1
du -sh *

这个命令用来查看根目录下,所有文件的大小分布

比如,我的电脑 Library 文件路径最大

那就在进入 Library 文件路径,再执行 du -sh *

直至找到占用内存最大的文件,然后结合实际情况,进行删减

问题描述

在 SpringSecurity 中,我想配置一个关于session并发的控制,于是我是这样配置的

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.invalidSessionStrategy(new InvalidSessionStrategyImpl())
.maximumSessions(-1).expiredSessionStrategy(expiredSessionStrategy())//配置并发登录,-1表示不限制
.sessionRegistry(sessionRegistry());
}

上下文的配置我在此省略了

这里设置 maximumSessions 为 -1,表示不限制同一账号登录的客户端数

session过期后执行的逻辑是进入我自定义的类 expiredSessionStrategy() 中

因为我是构建的 rest 服务,所以我是返回的 http 状态码

1
2
3
4
5
6
7
8
public class ExpiredSessionStrategyImpl implements SessionInformationExpiredStrategy {

@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
event.getResponse().sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
JSONObject.toJSONString(MessageBody.failure(405,"not login or login has been expired")));
}
}

在这里,问题就来了

我测试的时候,把 -1 改成了 1,之后登录同一个用户,后面登录的用户会把前面一个已经登录的用户挤下线,就是说之前登录的那个用户的session 会过期

就是说他所在的页面再发送任何请求的话会收到我返回的 405 状态码

在这里是没问题的

问题就在发完一个请求后,在发一个请求,在浏览器的 network 上会看到发出的请求会被重定向的 /login 请求上

后续再发任何请求都会被重定向到 /login 上

问题思考

为什么会出现这样的情况呢?

为什么会第一个请求会收到405的状态码,后续的请求会被重定向到 /login 呢?

通过 debug 断点,我定位到过滤器的前置执行方法 beforeInvocation() 上

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
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();

if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);

if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}

if (debug) {
logger.debug("Public object - authentication not attempted");
}

publishEvent(new PublicInvocationEvent(object));

return null; // no further work post-invocation
}

if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}

if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}

Authentication authenticated = authenticateIfRequired();

// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));

throw accessDeniedException;
}

if (debug) {
logger.debug("Authorization successful");
}

if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}

// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);

if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}

// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}

SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);

// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}

问题出在了 SecurityContextHolder.getContext().getAuthentication() == null

getAuthentication() 为 null,于是进入了credentialsNotFound(),抛出了 AuthenticationCredentialsNotFoundException 异常

确实,在控制台上也能看到抛出的异常信息

问题深入

AuthenticationCredentialsNotFoundException 是 AuthenticationException 异常的子类

不仅仅是 AuthenticationCredentialsNotFoundException 还有其他很多异常都是异常的子类

既然抛出了异常,猜测肯定是被某个处理器给处理了而且处理的默认机制是重定向到 /login

于是继续搜索 SpringSecurity 异常处理器

我找到的答案是 ExceptionTranslationFilter

ExceptionTranslationFilter 是Spring Security的核心filter之一,用来处理AuthenticationException和AccessDeniedException两种异常(由FilterSecurityInterceptor认证请求返回的异常)

ExceptionTranslationFilter 对异常的处理是通过这两个处理类实现的,处理规则很简单:

规则1. 如果异常是 AuthenticationException,使用 AuthenticationEntryPoint 处理
规则2. 如果异常是 AccessDeniedException 且用户是匿名用户,使用 AuthenticationEntryPoint 处理
规则3. 如果异常是 AccessDeniedException 且用户不是匿名用户,如果否则交给 AccessDeniedHandler 处理。

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
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);

sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);

sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);

accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}

我们这里的异常是 AuthenticationException ,紧接着就找 sendStartAuthentication() 方法

1
2
3
4
5
6
7
8
9
10
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}

上面的方法是先保存请求,之后执行 authenticationEntryPoint.commence(request, response, reason), 再深入来看

默认实现 commence 接口的是 LoginUrlAuthenticationEntryPoint 类

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
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

String redirectUrl = null;

if (useForward) {

if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}

if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);

if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}

RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

dispatcher.forward(request, response);

return;
}
}
else {
// redirect to login page. Use https if forceHttps true

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

}

redirectStrategy.sendRedirect(request, response, redirectUrl);
}

我们看到了 redirectUrl = buildRedirectUrlToLoginPage(request, response, authException)

这下总算是知道了为什么会重定向了 /login 请求了

问题解决

知道问题的原因了,解决问题就很简单了,重新实现 commence 接口,返回http 状态码就可以了,于是加上这样的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.invalidSessionStrategy(new InvalidSessionStrategyImpl())
.maximumSessions(-1).expiredSessionStrategy(expiredSessionStrategy())//配置并发登录,-1表示不限制
.sessionRegistry(sessionRegistry())
.and()
.and()
.exceptionHandling()
.authenticationEntryPoint(new UnauthenticatedEntryPoint())
.accessDeniedHandler(new AuthorizationFailure());
}
1
2
3
4
5
6
7
8
public class UnauthenticatedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
if (!response.isCommitted()) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,"未认证的用户:" + authException.getMessage());
}
}
}

再次重试,发现会返回 405状态码了,不会在重定向到 /login 了

问题解决

引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

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

SpringSecurity 配置类

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
@EqualsAndHashCode(callSuper = true)
@Data
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@ConfigurationProperties(prefix = "spring.security.ignore")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private List<String[]> marchers;

@Bean
public UserService userService(){
return new UserService();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable()
.csrf().disable()
.authorizeRequests()
// .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() //解决PreFlight请求问题
.anyRequest().authenticated()//其他请求必须授权后访问
.and()
.formLogin()
// .loginPage("/login")
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.permitAll()//登录请求可以直接访问
.and()
.logout()
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(new LogoutSuccess())
.permitAll()//注销请求可直接访问
.and()
.sessionManagement()
.invalidSessionStrategy(new InvalidSessionStrategyImpl())
.maximumSessions(-1).expiredSessionStrategy(expiredSessionStrategy())//配置并发登录,-1表示不限制
.sessionRegistry(sessionRegistry())
.and()
.and()
.exceptionHandling()
.authenticationEntryPoint(new UnauthenticatedEntryPoint())
.accessDeniedHandler(new AuthorizationFailure())
.and()
.addFilterBefore(new AuthorizationFilter(new AuthorizationMetadataSource(), new
AuthorizationAccessDecisionManager()), FilterSecurityInterceptor.class);

}

@Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider());
}

@Bean
public SessionRegistry sessionRegistry(){
return new SessionRegistryImpl();
}

@Bean
public ExpiredSessionStrategyImpl expiredSessionStrategy(){
return new ExpiredSessionStrategyImpl();
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return SecurityUtils.getPasswordEncoder();
}

@Override
public void configure(WebSecurity web) {
for (String[] marcher : marchers) {
web.ignoring().antMatchers(marcher);
}
}

@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
/*不将UserNotFoundExceptions转换为BadCredentialsException*/
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(userService());
provider.setPasswordEncoder(passwordEncoder());
return provider;
}

@Bean
public AuthenticationSuccess authenticationSuccessHandler(){
return new AuthenticationSuccess();
}

@Bean
public AuthenticationFailureHandler authenticationFailureHandler(){
return new AuthenticationFailure();
}
}

自定义 userService

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
public class UserService implements UserDetailsService {
@Autowired
private WebSecurityConfig securityConfig;

@Autowired
private SysUserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
throw new UsernameNotFoundException("用户名不能为空!");
}
SysUser user = new SysUser();
user.setLoginName(username);
SysUser queryUser = userMapper.selectOne(user);
if (null == queryUser) {
throw new UsernameNotFoundException("用户 " + username + " 不存在!");
}
if (!queryUser.getPermissionIpList().contains("0.0.0.0") && !queryUser.getPermissionIpList().contains
(SecurityUtils.getRemoteAddress())) {
throw new InvalidIpAddrException("登录 IP 地址不合法");
}

return new SecurityUser(queryUser);
}

/**
* 重新授权
*/
public void reAuthorization(){
SecurityUser user = SecurityUtils.currentUser();
assert user != null;
String username = user.getUsername();
user.setRoles(userMapper.findRolesByName(username));
user.setMenus(userMapper.findMenusByName(username));
user.setFunctions(userMapper.findFunctionsByName(username));

List<GrantedAuthority> authorities = new ArrayList<>();
for (Function function : user.getFunctions()) {
for (String url : function.getFunctionUrl().split(",")) {
authorities.add(new SimpleGrantedAuthority(url));
}
}
user.setAuthorities(authorities.stream().distinct().collect(Collectors.toList()));
// 得到当前的认证信息
Authentication auth = SecurityUtils.getAuthentication();
// 生成新的认证信息
Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), authorities);
// 重置认证信息
SecurityContextHolder.getContext().setAuthentication(newAuth);

}


/**
* 根据用户名 将该用户登录的所有账户踢下线
* @param userNames userNames
*/
public void kickOutUser(String... userNames) {
SessionRegistry sessionRegistry = securityConfig.sessionRegistry();
for (Object o : sessionRegistry.getAllPrincipals()) {
SecurityUser user = (SecurityUser) o;
for (String username : userNames) {
if (user.getLoginName().equals(username)) {
for (SessionInformation sessionInformation : sessionRegistry.getAllSessions(user, false)) {
sessionInformation.expireNow();
}
}
}
}
}
}

用户实体类 SecurityUser

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
@Data
public class SecurityUser extends SysUser implements UserDetails{

/*角色*/
private List<SysRole> roles;

/*菜单*/
private List<Menu> menus;

/*功能权限*/
private List<Function> functions;

private Collection<? extends GrantedAuthority> authorities;

SecurityUser(SysUser user) {
this.setUserId(user.getUserId());
this.setGlbm(user.getGlbm());
this.setXh(user.getXh());
this.setLoginName(user.getLoginName());
this.setLoginPassword(user.getLoginPassword());
this.setPermissionIpList(user.getPermissionIpList());
this.setLatestLoginTime(user.getLatestLoginTime());
this.setTotalLoginCounts(user.getTotalLoginCounts());
this.setName(user.getName());
this.setCreateTime(user.getCreateTime());
this.setUpdateTime(user.getUpdateTime());
}


@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public String getPassword() {
return super.getLoginPassword();
}

@Override
public String getUsername() {
return super.getLoginName();
}

@Override
public int hashCode() {
return this.getLoginName().hashCode();
}

@Override
public boolean equals(Object obj) {
return obj instanceof SecurityUser && ((SecurityUser) obj).getLoginName().equals(this.getLoginName());
}
}

授权Filter

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
public class AuthorizationFilter extends AbstractSecurityInterceptor implements Filter {

private AuthorizationMetadataSource metadataSource;

public AuthorizationFilter(AuthorizationMetadataSource metadataSource, AuthorizationAccessDecisionManager
accessDecisionManager) {
this.metadataSource = metadataSource;
this.setAccessDecisionManager(accessDecisionManager);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}

private void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}


@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return metadataSource;
}

@Override
public void destroy() {
}

@Override
public void init(FilterConfig filterConfig) {
}
}

授权访问决策器

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
public class AuthorizationAccessDecisionManager implements AccessDecisionManager {

/**
* 认证用户是否具有权限访问该url地址
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
String url = ((FilterInvocation) object).getRequestUrl();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
SimpleGrantedAuthority authority = (SimpleGrantedAuthority) grantedAuthority;
if (matches(authority.getAuthority(), request)) {
return;
}
}
throw new AccessDeniedException("uri: " + url + ",无权限访问!");
}

/**
* 当前AccessDecisionManager是否支持对应的ConfigAttribute
*/
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

/**
* 当前AccessDecisionManager是否支持对应的受保护对象类型
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}

private boolean matches(String url, HttpServletRequest request) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
return matcher.matches(request);
}
}

授权元数据

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
public class AuthorizationMetadataSource implements FilterInvocationSecurityMetadataSource {

/**
* 加载 请求的url资源所需的权限
* @param object object
* @return Collection
* @throws IllegalArgumentException Exception
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String url = ((FilterInvocation) object).getRequestUrl();
Collection<ConfigAttribute> configAttributes = new ArrayList<>();
configAttributes.add(new SecurityConfig(url));
return configAttributes;
}

/**
* 会在启动时加载所有 ConfigAttribute 集合
* @return Collection
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

封装一些 Security 工具类

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
public class SecurityUtils {

public static Authentication getAuthentication(){
return SecurityContextHolder.getContext().getAuthentication();
}

/**
* 用户是否登录
* @return boolean
*/
public static boolean isAuthenticated(){
return getAuthentication() != null || !(getAuthentication() instanceof AnonymousAuthenticationToken);
}

/**
* 获取当前用户
* @return user
*/
public static SecurityUser currentUser(){
if (isAuthenticated()) {
return (SecurityUser) getAuthentication().getPrincipal();
}
return null;
}

/**
* 获取 webAuthenticationDetails
*/
private static WebAuthenticationDetails webAuthenticationDetails(){
return (WebAuthenticationDetails)getAuthentication().getDetails();
}

/**
* 获取session id
*/
public static String getSessionId(){
return webAuthenticationDetails().getSessionId();
}

/**
* 获取远程访问地址
*/
public static String getRemoteAddress(){
return webAuthenticationDetails().getRemoteAddress();
}

/**
* 获取密码编译器
* @return BCryptPasswordEncoder
*/
public static BCryptPasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder(4);
}

/**
* 根据明文加密 返回密文
* @param rawPassword 明文
* @return 密文
*/
public static String createPassword(String rawPassword){
return getPasswordEncoder().encode(rawPassword.trim());
}

/**
* 传入明文和密文 检查是否匹配
* @param rawPassword 明文
* @param encodedPassword 密文
* @return boolean
*/
public static boolean isMatching(String rawPassword,String encodedPassword){
return getPasswordEncoder().matches(rawPassword,encodedPassword);
}

}

主要的实现类都列举在内了,还有一些成功和失败的处理类,再次没有列举出来
因为该项目为构建纯restful风格的后台项目,这些成功或失败的处理类基本都是返回的http状态码

环境准备

  • 项目整合 通用 mapper 和 pagehelper 插件,这部分以前有写过,略
  • 需要集成 mybatis 的 generator 插件,方便自动生成 实体类和 mapper 类,还可以生成xml,不过一般我们都不用 xml
  • baseMapper 需要继承 ExampleMapper 不过只需要继承 Mapper 就可以了,因为 Mapper 已经继承了 ExampleMapper

Example 的用法

首先需要说明一点 ,和 Example 使用相同的还有 Condition 类 该类继承自 Example,使用方法和 Example 完全一样,只是为了避免语义有歧义重命名的一个类,这里我们都用 Example 来说明

  • 创建 Example :
1
Example example = new Example(XXX.class);

其中构造方法为生成的 model 实体类,还有 2 个构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* 带exists参数的构造方法,默认notNull为false,允许为空
*
* @param entityClass
* @param exists - true时,如果字段不存在就抛出异常,false时,如果不存在就不使用该字段的条件
*/
public Example(Class<?> entityClass, boolean exists) {
...
}

/**
* 带exists参数的构造方法
*
* @param entityClass
* @param exists - true时,如果字段不存在就抛出异常,false时,如果不存在就不使用该字段的条件
* @param notNull - true时,如果值为空,就会抛出异常,false时,如果为空就不使用该字段的条件
*/
public Example(Class<?> entityClass, boolean exists, boolean notNull) {
...
}

然后可以对 example 的实体类的单表进行查询了

1
2
3
4
Example example = new Example(XXX.class);
example.createCriteria().andGreaterThan("id", 100).andLessThan("id",151);
example.or().andLessThan("id", 41);
List<XXX> list = mapper.selectByExample(example);

以上查询的条件是,查询 id 大于 100 并且小于 151 或者 id 小于 41 的记录

还可以写成 sql 的方式:

1
2
3
4
5
6
Example example = new Example(XXX.class);
example.createCriteria().andCondition("id > 100 and id <151 or id < 41");

// andCondition() 方法可以叠加使用,像这样
example.createCriteria().andCondition("id > 100 and id <151").orCondition("id <41");

andCondition() 有2中使用方法:
andCondition(String condition) : 手写条件,例如 “length(name)<5”
andCondition(String condition, Object value) : 手写左边条件,右边用value值,例如 “length(name)=” “5”
orCondition() 也是类似的

example 里有很多 mysql 常用的方法,使用方法和 elasticsearch 的 java api 很类似,这里列举几个

  • Set<String> selectColumns : 查询的字段
  • Set<String> excludeColumns : 排除的查询字段
  • Map<String, EntityColumn> propertyMap : 属性和列对应
  • andAllEqualTo : 将此对象的所有字段参数作为相等查询条件,如果字段为 null,则为 is null
  • andGreaterThan : and 条件 大于
  • andBetween : and 条件 between
  • andEqualTo : 将此对象的不为空的字段参数作为相等查询条件 还有一种有 value 参数的是 = 条件
  • andGreaterThanOrEqualTo : and 条件 》=

还有一些一看就知道意思的

  • andIn
  • andIsNotNull
  • andIsNull
  • andLessThan
  • andLessThanOrEqualTo
  • andNotLike

上面是以 and 条件举例 ,or的条件也是一样的

集成分页功能

我们知道 PageHelper.startPage(pageNum, pageSize); 可以对 后面的一个 select 进行分页
那么我们可以对 example 进行一个分页查询的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// 在baseMapper 里封装一个接口
PageInfo selectPageByExample(int pageNum, int pageSize, Object example);

//这样实现上面的接口
@Override
public PageInfo selectPageByExample(int pageNum, int pageSize, Object example) {
PageHelper.startPage(pageNum, pageSize);
List<T> list = selectByExample(example);
return new PageInfo<>(list);
}

//java 8 的lamda 用法
@Override
public PageInfo selectPageByExample(int pageNum, int pageSize, Object example) {
return PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(()->baseMapper.selectByExample(example));
}

前言

SpringBoot 默认有2种打包方式,一种是直接打成 jar 包,直接使用 java -jar 跑起来,另一种是打成 war 包,移除掉 web starter 里的容器依赖,然后丢到外部容器跑起来。

第一种方式的缺点是整个项目作为一个 jar,部署到生产环境中一旦有配置文件需要修改,则过程比较麻烦
linux 下可以使用 vim jar 包,找到配置文件修改后再保存
window 下需要使用 解压缩软件打开 jar 再找到配置文件,修改后替换更新

第二种方式的缺点是需要依赖外部容器,这无非多引入了一部分,很多时候我们很不情愿这么做

spring boot 项目启动时 指定配置有2种方式:一种是启动时修改配置参数,像 java -jar xxxx.jar –server.port=8081 这样;另外一种是 指定外部配置文件加载,像 java -jar xxxx.jar -Dspring.config.location=applixxx.yml这样

目标

我们希望打包成 tomcat 或者 maven 那样的软件包结构,即

--- bin
    --- start.sh
    --- stop.sh
    --- restart.sh
    --- start.bat
    --- stop.bat
    --- restart.bat
--- boot
    --- xxxx.jar
--- lib
--- conf
--- logs
--- README.md
--- LICENSE

就像这样
Assembly-Package

  • bin 目录放一些我们程序的启动停止脚本
  • boot 目录放我们自己的程序包
  • lib 目录是我们程序的依赖包
  • conf 目录是项目的配置文件
  • logs 目录是程序运行时的日志文件
  • README.md 使用说明
  • LICENSE 许可说明

准备

  • maven-jar-plugin : 打包我们写的程序包和所需的依赖包,并指定入口类,依赖包路径和classpath路径,其实就是在MANIFEST.MF这个文件写入相应的配置
  • maven-assembly-plugin : 自定义我们打包的文件目录的格式

pom.xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<build>
<plugins>
<!--<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<mainClass>com.ahtsoft.AhtsoftBigdataWebApplication</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>../lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Class-Path>../conf/resources/</Class-Path>
</manifestEntries>
</archive>
<excludes>
<exclude>static/**</exclude>
<exclude>*.yml</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
  1. 将 spring boot 默认的打包方式 spring-boot-maven-plugin 去掉,使用现在的打包方式
  2. maven-jar-plugin 配置中,制定了入口类,addClasspath 配置将所需的依赖包单独打包,依赖包打的位置在lib目录底下,在MANIFEST.MF这个文件写入相应的配置
  3. 配置了 classpath 在 /conf/resources/ ,这个和后面的 assembly.xml 要相对应
  4. 我单独把spring boot 的配置文件 yml文件 和 静态资源目录 static 单独拎了出来,在我们的源码包中并没有打进去,而是交给 assembly.xml 来单独打到一个独立的文件 conf文件下
  5. 这也是照应了 前面为什么要设置 classpath 为 /conf/resources/

下面重要的是 assembly.xml 配置文件了,这个文件才是把我们的程序打成标准的目录结构

assembly.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<assembly>
<id>assembly</id>
<formats>
<format>tar.gz</format>
</formats>
<baseDirectory>${project.artifactId}-${project.version}/</baseDirectory>

<files>
<file>
<source>target/${project.artifactId}-${project.version}.jar</source>
<outputDirectory>boot/</outputDirectory>
<destName>${project.artifactId}-${project.version}.jar</destName>
</file>
</files>

<fileSets>
<fileSet>
<directory>./</directory>
<outputDirectory>./</outputDirectory>
<includes>
<include>*.txt</include>
<include>*.md</include>
</includes>
</fileSet>
<fileSet>
<directory>src/main/bin</directory>
<outputDirectory>bin/</outputDirectory>
<includes>
<include>*.sh</include>
<include>*.cmd</include>
</includes>
<fileMode>0755</fileMode>
</fileSet>
<fileSet>
<directory>src/main/resources/static</directory>
<outputDirectory>conf/resources/static/</outputDirectory>
<includes>
<include>*</include>
</includes>
</fileSet>
<fileSet>
<directory>src/main/resources</directory>
<outputDirectory>conf/resources</outputDirectory>
<includes>
<include>*.properties</include>
<include>*.conf</include>
<include>*.yml</include>
</includes>
</fileSet>
</fileSets>

<dependencySets>
<dependencySet>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<scope>runtime</scope>
<includes>
<include>*:*</include>
</includes>
<excludes>
<exclude>${groupId}:${artifactId}</exclude>
<exclude>org.springframework.boot:spring-boot-devtools</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>
  • 将最终的程序包打成 tar.gz ,当然也可以打成其他的格式如zip,rar等,fileSets 里面指定我们源码里的文件和路径打成标准包相对应的目录
  • 需要注意的是,在最终的依赖库 lib 下 去掉我们的程序和开发时spring boot的热部署依赖 spring-boot-devtools,否则的会出问题
  • 代码里的启动和停止脚本要赋予权限,否则在执行的时候可能提示权限的问题

使用说明

上次自己写这篇文章 已经是今年初了,一年过去了, Spring Boot 项目在不停的更新着,与此同时其他的 stater项目也在不停的更新着,今天就来重新整合下Druid,MyBatis,通用 Mapper,PageHelper,打算在企业级项目中使用

当前 SpringBoot 最新的发布版是 1.5.9.RELEASE
昨天还是 1.5.8,今天发现就是1.5.9.RELEASE了
本篇文章搭建的脚手架就是基于 1.5.9.RELEASE

年初我自己搭建这个脚手架使用的时候,那时 Druid,MyBatis,Mapper,PageHelper,这几个开源项目都没有集成 SpringBoot,我自己还是使用 JavaConfig 配置的
现在不一样了,一年过去了,这些项目的作者也开发了对 SpringBoot 支持的 starter 版本

本篇文章就来整合这些开源框架制作一个脚手架

另外是有打算将它应用到企业级项目中的

版本

SpringBoot: 1.5.9.RELEASE
SpringBoot-mybatis : 1.3.1
mapper-spring-boot-starter : 1.1.5
pagehelper-spring-boot-starter: 1.2.3
druid-spring-boot-starter: 1.1.5

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.ahtsoft</groupId>
<artifactId>ahtsoft-bigdata-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>ahtsoft-bigdata-web</name>
<description>ahtsoft bigData Web Project</description>

<developers>
<developer>
<name>LiuFa</name>
<email>liuf@ahtsoft.com</email>
</developer>
</developers>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>-->

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>

<!--mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.1.5</version>
</dependency>

<!--pagehelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>

<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.5</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

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

<!--<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>-->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

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

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

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

<!--<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>-->

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>


</project>

说明:
pom 中除了应用了必要的依赖,还引入了SpringSecurity, 打算做脚手架的安全认证
caffeine cache 打算做 restapi 的 cache 的

使用 devtools 开发时热部署
lombok 简化代码配置
websocket 全双工通信
集群的话 spring-session 做到 session 共享

application,yml

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
spring:
datasource:
druid:
url: jdbc:mysql://lfdevelopment.cn:3333/boot-security?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: q1pE3gb8+1Q9DkE27wjl0Q1xhiYJJC0w5+TJIZXjEW9fKv9W2h4VOSWOajAVtNXRjaDhtXZlyWN8SAJPqzNFqg==
driver-class-name: com.mysql.jdbc.Driver
public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9HqNyD1g+vgwITT4x5EcaWKGJQ7/HCl1C0Uwc8AHPr2y7heJBLGdWtvIKtRKGsn4LCCkyKfVFs87nKKFpJbPECAwEAAQ==
connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.druid.public-key}
filter:
config:
enabled: true
mybatis:
type-aliases-package: com.ahtsoft.**.model
configuration:
map-underscore-to-camel-case: true
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper:
mappers[0]: com.ahtsoft.config.basemapper.BaseMapper
not-empty: false
identity: MYSQL
pagehelper:
helper-dialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
logging:
level:
com:
ahtsoft: debug

正常配置了druid 的连接配置,其中使用 ConfigTool 的密码加密功能,提供加密后的密文个公钥,在连接数据库时会自动解密

mybatis 配置了各个 model 的位置,配置开启驼峰命名转换,SQL 语句的打印使用的 springboot 的日志功能,将实现的 StdOutImpl给注释了
配置的分页插件 pagehelper 参数

最后在@ SpringBoot 注解下加入
@MapperScan(basePackages = "com.ahtsoft.**.mapper")
用来扫描 mapper自动注入为 bean

项目地址: https://github.com/JoyLau/ahtsoft-bigdata-web.git

push 添加最后一项

在数组末尾添加一项,并返回数组的长度, 可以添加任意类型的值作为数组的一项。

1
2
3
4
5
var arr = [1,2];
arr.push(6) // arr: [1,2,6]
arr.push('aa') // arr: [1,2,6,"aa"]
arr.push(undefined) // arr: [1,2,6,"aa",undefined]
arr.push({a: "A", b: "B"}) // [1,2,6,"aa",undefined,{a: "A", b: "B"}]

unshift 在最前面添加一项

1
2
3
var arr = [1,2];
arr.unshift(9) // [9, 1, 2]
arr.unshift('aa') // ['aa',9, 1, 2]

pop 删除最后一项

删除最后一项,并返回删除元素的值;如果数组为空则返回undefine。对数组本身操作

1
2
3
var arr = [1,2,3,4,5];
arr.pop() // arr: [1, 2, 3, 4]
arr.pop() // arr: [1, 2, 3]

shift 删除最前面一项

1
2
3
var arr = [1,2,3,4,5];
arr.shift() // [2, 3, 4, 5]
arr.shift() // [3, 4, 5]

slice截取(切片)数组 得到截取的数组

不改变原始数组,得到新的数组

slice(start,end)

1
2
3
4
var arr = [1,2,3,4,5];
var a = arr.slice(1) // a: [2,3,4,5]
var a = arr.slice(1,3) // a: [2,3]
var a = arr.slice(3,4) // a: [5]

splice剪接数组

改变原数组,可以实现shift前删除,pop后删除,unshift前增加,同push后增加一样的效果。索引从0开始

splice(index,howmany,item1,…..,itemX)

1
2
3
4
5
6
7
8
9
10
11
var arr = [1,2,3,4,5];

push: arr.splice(arr.length, 0, 6) // [1, 2, 3, 4, 5, 6]
unshift: arr.splice(0, 0, 6) // [6, 1, 2, 3, 4, 5]
pop: arr.splice(arr.length-1, 1) // [1, 2, 3, 4]
shift: arr.splice(0, 1) // [2, 3, 4, 5]

arr.splice(1) // [1]
arr.splice(1, 2) // [1, 4, 5]
arr.splice(1, 0, 'A') // [1, "A",2,3, 4, 5]
arr.splice(1, 2, 'A', 'B') // [1, "A", "B", 4, 5]

concat 数组合并

合并后得到新数组,原始数组不改变

1
2
3
var arr1 = [1,2];
var arr2 = [3,4,5];
var arr = arr1.concat(arr2) // [1,2,3,4,5]

indexOf 数组元素索引

并返回元素索引,不存在返回-1,索引从0开始

1
2
3
4
5
var arr = ['a','b','c','d','e']; 
arr.indexOf('a'); //0
arr.indexOf(a); //-1
arr.indexOf('f'); //-1
arr.indexOf('e'); //4

join 数组转字符串

1
2
3
var a, b;
a = [0, 1, 2, 3, 4];
b = a.join("-"); // 0-1-2-3-4

reverse 数组翻转

并返回翻转后的原数组,原数组翻转了

1
2
var a = [1,2,3,4,5]; 
a.reverse()//a:[5, 4, 3, 2, 1] 返回[5, 4, 3, 2, 1]

数组里面的对象去重复

1
2
3
4
5
6
7
8
9
10
11
unique(arr){
let hash = {};
arr = arr.reduce(function(item, next) {
if (!hash[next.name]) {
item.push(next);
hash[next.name] = true;
}
return item
}, []);
return arr;
}

arr.forEach(callback)

遍历数组,无return
callback的参数:
value –当前索引的值
index –索引
array –原数组

arr.map(callback)

映射数组(遍历数组),有return 返回一个新数组
callback的参数:
value –当前索引的值
index –索引
array –原数组

注意: arr.forEach()和arr.map()的区别

  1. arr.forEach()是和for循环一样,是代替for。arr.map()是修改数组其中的数据,并返回新的数据。
  2. arr.forEach() 没有return arr.map() 有return

arr.filter(callback)

过滤数组,返回一个满足要求的数组

arr.every(callback)

依据判断条件,数组的元素是否全满足,若满足则返回ture

1
2
3
4
5
let arr = [1,2,3,4,5]
let arr1 = arr.every( (i, v) => i < 3)
console.log(arr1) // false
let arr2 = arr.every( (i, v) => i < 10)
console.log(arr2) // true

arr.some()

依据判断条件,数组的元素是否有一个满足,若有一个满足则返回ture

1
2
3
4
5
let arr = [1,2,3,4,5]
let arr1 = arr.some( (i, v) => i < 3)
console.log(arr1) // true
let arr2 = arr.some( (i, v) => i > 10)
console.log(arr2) // false

arr.reduce(callback, initialValue)

迭代数组的所有项,累加器,数组中的每个值(从左到右)合并,最终计算为一个值

参数:

  1. callback:
    previousValue 必选 –上一次调用回调返回的值,或者是提供的初始值(initialValue)
    currentValue 必选 –数组中当前被处理的数组项
    index 可选 –当前数组项在数组中的索引值
    array 可选 –原数组

  2. initialValue: 可选 –初始值
    实行方法:回调函数第一次执行时,preValue 和 curValue 可以是一个值,如果 initialValue 在调用 reduce() 时被提供,那么第一个 preValue 等于 initialValue ,并且curValue 等于数组中的第一个值;如果initialValue 未被提供,那么preValue 等于数组中的第一个值.

1
2
3
4
5
let arr = [0,1,2,3,4]
let arr1 = arr.reduce((preValue, curValue) =>
preValue + curValue
)
console.log(arr1) // 10

arr.find(callback)

find的参数为回调函数,回调函数可以接收3个参数,值x、所以i、数组arr,回调函数默认返回值x。

1
2
3
4
5
6
7
let arr=[1,2,234,'sdf',-2];
arr.find(function(x){
return x<=2;
})//结果:1,返回第一个符合条件的x值
arr.find(function(x,i,arr){
if(x<2){console.log(x,i,arr)}
})//结果:1 0 [1, 2, 234, "sdf", -2],-2 4 [1, 2, 234, "sdf", -2]

arr.findIndex(callback)

findIndex和find差不多,不过默认返回的是索引。

arr.includes()

includes函数与string的includes一样,接收2参数,查询的项以及查询起始位置。

1
2
3
4
let arr=[1,2,234,'sdf',-2];
arr.includes(2);// 结果true,返回布尔值
arr.includes(20);// 结果:false,返回布尔值
arr.includes(2,3)//结果:false,返回布尔值

arr.keys()

keys,对数组索引的遍历

1
2
3
4
let arr=[1,2,234,'sdf',-2];
for(let a of arr.keys()){
console.log(a)
}//结果:0,1,2,3,4 遍历了数组arr的索引

arr.values()

values, 对数组项的遍历

1
2
3
4
let arr=[1,2,234,'sdf',-2];
for(let a of arr.values()){
console.log(a)
}//结果:1,2,234,sdf,-2 遍历了数组arr的值

arr.entries()

entries,对数组键值对的遍历。

1
2
3
4
5
6
7
let arr=['w','b'];
for(let a of arr.entries()){
console.log(a)
}//结果:[0,w],[1,b]
for(let [i,v] of arr.entries()){
console.log(i,v)
}//结果:0 w,1 b

arr.fill()

fill方法改变原数组,当第三个参数大于数组长度时候,以最后一位为结束位置。

1
2
3
4
let arr=['w','b'];
arr.fill('i')//结果:['i','i'],改变原数组
arr.fill('o',1)//结果:['i','o']改变原数组,第二个参数表示填充起始位置
new Array(3).fill('k').fill('r',1,2)//结果:['k','r','k'],第三个数组表示填充的结束位置

Array.of()

Array.of()方法永远返回一个数组,参数不分类型,只分数量,数量为0返回空数组。

1
2
3
4
Array.of('w','i','r')//["w", "i", "r"]返回数组
Array.of(['w','o'])//[['w','o']]返回嵌套数组
Array.of(undefined)//[undefined]依然返回数组
Array.of()//[]返回一个空数组

arr.copyWithin

copyWithin方法接收三个参数,被替换数据的开始处、替换块的开始处、替换块的结束处(不包括);copyWithin(s,m,n).

1
2
3
4
["w", "i", "r"].copyWithin(0)//此时数组不变
["w", "i", "r"].copyWithin(1)//["w", "w", "i"],数组从位置1开始被原数组覆盖,只有1之前的项0保持不变
["w", "i", "r","b"].copyWithin(1,2)//["w", "r", "b", "b"],索引2到最后的r,b两项分别替换到原数组1开始的各项,当数量不够,变终止
["w", "i", "r",'b'].copyWithin(1,2,3)//["w", "r", "r", "b"],强第1项的i替换为第2项的r

Array.from()

Array.from可以把带有lenght属性类似数组的对象转换为数组,也可以把字符串等可以遍历的对象转换为数组,它接收2个参数,转换对象与回调函数

1
2
3
4
5
6
7
8
9
10
Array.from({'0':'w','1':'b',length:2})//["w", "b"],返回数组的长度取决于对象中的length,故此项必须有!
Array.from({'0':'w','1':'b',length:4})//["w", "b", undefined, undefined],数组后2项没有属性去赋值,故undefined
Array.from({'0':'w','1':'b',length:1})//["w"],length小于key的数目,按序添加数组

//////////////////////////////
let divs=document.getElementsByTagName('div');
Array.from(divs)//返回div元素数组
Array.from('wbiokr')//["w", "b", "i", "o", "k", "r"]
Array.from([1,2,3],function(x){
return x+1})//[2, 3, 4],第二个参数为回调函数

arr.sort(callback)

如果方法没有使用参数,那么将按照字母顺序对数组元素进行排序

1
2
3
4
5
6
7
8
9
10
11
var arr = [
{name:'zopp',age:0},
{name:'gpp',age:18},
{name:'yjj',age:8}
];
var compare = age => {
return (a,b) => {
return a[age] - b[age];
}
}
arr.sort(compare(age))

arr.indexOf()

从前往后遍历,返回item在数组中的索引位,如果没有返回-1;通常用来判断数组中有没有某个元素。可以接收两个参数,第一个参数是要查找的项,第二个参数是查找起点位置的索引

arr.lastIndexOf()

与indexOf一样,区别是从后往前找。

arr.flat()

数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维数组。该方法返回一个新数组,对原数据没有影响
flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1

1
2
3
4
5
6
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

arr.flatMap()

flatMap()方法对原数组的每个成员执行一个函数,相当于执行Array.prototype.map(),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

1
2
3
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

发现一个比较好的js组件,地址: https://www.lodashjs.com/ 里面有很多关于对数组的操作

准备工作

首先

首先要说明的是,本篇文章用的 Spark 的版本都是目前最新版,直接在官网上下载就可以了,有注意的,下面详细说
有些命令可能已经不适应之前的旧版本了,以最新的版的为准
以下操作命令均是在服务的根目录下,使用的是相对目录

当前版本说明

  • jdk 1.8.0
  • Hadoop 版本2.8.2
  • 操作系统版本 centos 7.2
  • Spark 2.2.0

首先需要做的

安装 jdk 环境,再此不做详细叙述了,需要注意的是 jdk 的环境变量的配置
安装 Hadoop 环境,必须安装 Hadoop 才能使用 Spark,但如果使用 Spark 过程中没用到 HDFS,不启动 Hadoop 也是可以的

安装 Spark

打开官网下载的地址: http://spark.apache.org/downloads.html
需要注意的是,在选择下载包类型 Choose a package type 这个需要根据安装的 Hadoop 的版本来定的,或者直接选择 Pre-build with user-provided Apache Hadoop
这样我们可以自己配置 Hadoop 的版本

下载后,解压

进入 conf目录拷贝一份配置文件

1
cp ./conf/spark-env.sh.template ./conf/spark-env.sh

加入环境变量

1
export SPARK_DIST_CLASSPATH=$(/home/hadoop-2.8.2/bin/hadoop classpath)

我们运行

1
# ./sbin/start-all.sh

Spark 便会运行起来,查看地址 : http://localhost:8080 可查看到集群情况

运行 Spark 示例程序

正如前面的 Hadoop 一样, Spark 自带有很多示例程序,目录在 ./example 下面,有 Java 的 Python,Scala ,R 语言的,
这里我们选个最熟悉的 Java 版的来跑下

我们找到 Java 的目录里也能看到里面有很多程序,能看到我们熟悉的 wordcount

这里我们跑个 计算π的值

1
# ./bin/run-example SparkPi

运行后控制台打印很多信息,但是能看到这么一行:

Pi is roughly 3.1432557162785812

这就可以了

RDD

RDD : Spark 的分布式的元素集合(distributed collection of items),称为RDD(Resilient Distributed Dataset,弹性分布式数据集),它可被分发到集群各个节点上,进行并行操作。RDDs 可以通过 Hadoop InputFormats 创建(如 HDFS),或者从其他 RDDs 转化而来

我就简单的理解为 类比 Hadoop 的 MapReduce

RDDs 支持两种类型的操作

  • actions: 在数据集上运行计算后返回值
  • transformations: 转换, 从现有数据集创建一个新的数据集

Spark-Shell

Spark-shell 支持 Scala 和 Python 2中语言,这里我们就用 Scala 来做,关于 Scala 的使用和语法我打算新写一篇文章来记录下,
在之前我也写过 在 maven 中集成使用 Scala 来编程,这里我先用下

执行 shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ./bin/spark-shell

To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
17/11/24 09:33:36 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
17/11/24 09:33:37 WARN util.Utils: Your hostname, JoyLinux resolves to a loopback address: 127.0.0.1; using 10.0.2.15 instead (on interface enp0s3)
17/11/24 09:33:37 WARN util.Utils: Set SPARK_LOCAL_IP if you need to bind to another address
Spark context Web UI available at http://10.0.2.15:4040
Spark context available as 'sc' (master = local[*], app id = local-1511487218050).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 2.2.0
/_/

Using Scala version 2.11.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_151)
Type in expressions to have them evaluated.
Type :help for more information.

scala>

来执行一个文本统计

1
2
3
scala> val textFile = sc.textFile("file:///home/hadoop-2.8.2/input/test.txt").count()

textFile: Long = 4

默认读取的文件是 Hadoop HDFS 上的,上面的示例是从本地文件读取

来一个从 HDFS 上读取的,在这里我们之前在 HDFS 上传了个 tets.txt 的文档,在这里就可以直接使用了

1
2
3
4
scala> val textFile = sc.textFile("test2.txt");textFile.count()

textFile: org.apache.spark.rdd.RDD[String] = test2.txt MapPartitionsRDD[19] at textFile at <console>:26
res7: Long = 4

可以看到结果是一样的

Spark SQL 和 DataFrames

Spark SQL 是 Spark 内嵌的模块,用于结构化数据。在 Spark 程序中可以使用 SQL 查询语句或 DataFrame API。DataFrames 和 SQL 提供了通用的方式来连接多种数据源,支持 Hive、Avro、Parquet、ORC、JSON、和 JDBC,并且可以在多种数据源之间执行 join 操作。

下面仍在 Spark shell 中演示一下 Spark SQL 的基本操作,该部分内容主要参考了 Spark SQL、DataFrames 和 Datasets 指南。

Spark SQL 的功能是通过 SQLContext 类来使用的,而创建 SQLContext 是通过 SparkContext 创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala> var df = spark.read.json("file:///home/spark-2.2.0-bin-without-hadoop/examples/src/main/resources/employees.json")
df: org.apache.spark.sql.DataFrame = [name: string, salary: bigint]

scala> df.show()
+-------+------+
| name|salary|
+-------+------+
|Michael| 3000|
| Andy| 4500|
| Justin| 3500|
| Berta| 4000|
+-------+------+


scala>

再来执行2条查询语句
df.select("name").show()
df.filter(df("salary")>=4000).show()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scala> df.select("name").show()
+-------+
| name|
+-------+
|Michael|
| Andy|
| Justin|
| Berta|
+-------+


scala> df.filter(df("salary")>=4000).show()
+-----+------+
| name|salary|
+-----+------+
| Andy| 4500|
|Berta| 4000|
+-----+------+

执行一条 sql 语句试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scala> df.registerTempTable("employees")
warning: there was one deprecation warning; re-run with -deprecation for details

scala> spark.sql("select * from employees").show()
+-------+------+
| name|salary|
+-------+------+
|Michael| 3000|
| Andy| 4500|
| Justin| 3500|
| Berta| 4000|
+-------+------+


scala> spark.sql("select * from employees where salary >= 4000").show()
+-----+------+
| name|salary|
+-----+------+
| Andy| 4500|
|Berta| 4000|
+-----+------+

其实还有很多功能呢, http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.DataFrame ,这里先写2个试试,后续再细节学习

这篇文章暂时先写到这,还有后续的 Spark Streaming ,想先学学看流式计算Storm,之后对比下看看写一篇文章

接下来,熟悉 Scala 语法写一个 JavaScala 应用程序来通过 SparkAPI 单独部署一下试试

感受

这篇文章写下来等于将当时搭建 Spark 环境重复了一遍, 也是一遍敲命令,一遍记录下来,温故而知新,自己也学到不少东西,棒棒哒💯

0%