JoyLau's Blog

JoyLau 的技术学习与思考

引入依赖

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 环境重复了一遍, 也是一遍敲命令,一遍记录下来,温故而知新,自己也学到不少东西,棒棒哒💯

首先

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

当前版本说明

  • Hadoop 版本2.8.2
  • 操作系统版本 centos 7.2

首先需要做的

安装 jdk 环境,再此不做详细叙述了,需要注意的是 jdk 的环境变量的配置

yum install openjdk1.8xxxxx 这个安装的是 jre环境,并不是 jdk,安装 jdk

1
sudo yum install java-1.7.0-openjdk java-1.8.0-openjdk-devel

配置环境变量

1
vim ~/.bashrc

最后一行添加

1
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk

紧接着,让环境变量生效

1
source ~/.bashrc    # 使变量设置生效

设置好之后,再看下是否生效了

1
2
3
echo $JAVA_HOME     # 检验变量值
java -version
$JAVA_HOME/bin/java -version # 与直接执行 java -version 一样就没什么问题了

Hadoop 单机环境搭建及测试运行

官网下载 Hadoop 包

上传到服务器上,解压 tar -zxf hadoop-2.8.2.tar.gz
解压完了,我们可以查看下版本信息

1
2
3
4
5
6
7
8
bin/hadoop version

Hadoop 2.8.2
Subversion https://git-wip-us.apache.org/repos/asf/hadoop.git -r 66c47f2a01ad9637879e95f80c41f798373828fb
Compiled by jdu on 2017-10-19T20:39Z
Compiled with protoc 2.5.0
From source with checksum dce55e5afe30c210816b39b631a53b1d
This command was run using /home/hadoop-2.8.2/share/hadoop/common/hadoop-common-2.8.2.jar

出现上述信息就没有什么问题

接下来,就可以运行 Hadoop 自带的列子了,例子的目录在 /share/hadoop/mapreduce/hadoop-mapreduce-examples-2.8.2.jar

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
// 创建1个输入目录,输出目录不用创建,在命令中会自动创建,如果创建了,会提示目录已经存在,再次运行示例程序化,删除输出目录即可
mkdir ./input

// 看看都有哪些例子
./bin/hadoop jar ./share/hadoop/mapreduce/hadoop-mapreduce-examples-2.8.2.jar

An example program must be given as the first argument.
Valid program names are:
aggregatewordcount: An Aggregate based map/reduce program that counts the words in the input files.
aggregatewordhist: An Aggregate based map/reduce program that computes the histogram of the words in the input files.
bbp: A map/reduce program that uses Bailey-Borwein-Plouffe to compute exact digits of Pi.
dbcount: An example job that count the pageview counts from a database.
distbbp: A map/reduce program that uses a BBP-type formula to compute exact bits of Pi.
grep: A map/reduce program that counts the matches of a regex in the input.
join: A job that effects a join over sorted, equally partitioned datasets
multifilewc: A job that counts words from several files.
pentomino: A map/reduce tile laying program to find solutions to pentomino problems.
pi: A map/reduce program that estimates Pi using a quasi-Monte Carlo method.
randomtextwriter: A map/reduce program that writes 10GB of random textual data per node.
randomwriter: A map/reduce program that writes 10GB of random data per node.
secondarysort: An example defining a secondary sort to the reduce.
sort: A map/reduce program that sorts the data written by the random writer.
sudoku: A sudoku solver.
teragen: Generate data for the terasort
terasort: Run the terasort
teravalidate: Checking results of terasort
wordcount: A map/reduce program that counts the words in the input files.
wordmean: A map/reduce program that counts the average length of the words in the input files.
wordmedian: A map/reduce program that counts the median length of the words in the input files.
wordstandarddeviation: A map/reduce program that counts the standard deviation of the length of the words in the input files.

接下来,跑一个经典的 wordcount ,再次之前,我们创建一个文本以供程序统计

1
2
3
4
cat input/test.txt
vi input/test.txt

插入一些字符

开始记录

1
./bin/hadoop jar ./share/hadoop/mapreduce/hadoop-mapreduce-examples-2.8.2.jar wordcount ./input/test.txt ./output/

截取部分输出

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
17/11/22 11:30:08 INFO mapred.LocalJobRunner: reduce > reduce
17/11/22 11:30:08 INFO mapred.Task: Task 'attempt_local1247748922_0001_r_000000_0' done.
17/11/22 11:30:08 INFO mapred.LocalJobRunner: Finishing task: attempt_local1247748922_0001_r_000000_0
17/11/22 11:30:08 INFO mapred.LocalJobRunner: reduce task executor complete.
17/11/22 11:30:08 INFO mapreduce.Job: Job job_local1247748922_0001 running in uber mode : false
17/11/22 11:30:08 INFO mapreduce.Job: map 100% reduce 100%
17/11/22 11:30:08 INFO mapreduce.Job: Job job_local1247748922_0001 completed successfully
17/11/22 11:30:08 INFO mapreduce.Job: Counters: 30
File System Counters
FILE: Number of bytes read=605002
FILE: Number of bytes written=1267054
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
Map-Reduce Framework
Map input records=38
Map output records=35
Map output bytes=277
Map output materialized bytes=251
Input split bytes=103
Combine input records=35
Combine output records=23
Reduce input groups=23
Reduce shuffle bytes=251
Reduce input records=23
Reduce output records=23
Spilled Records=46
Shuffled Maps =1
Failed Shuffles=0
Merged Map outputs=1
GC time elapsed (ms)=21
Total committed heap usage (bytes)=461250560
Shuffle Errors
BAD_ID=0
CONNECTION=0
IO_ERROR=0
WRONG_LENGTH=0
WRONG_MAP=0
WRONG_REDUCE=0
File Input Format Counters
Bytes Read=140
File Output Format Counters
Bytes Written=165

看下输出情况

1
2
3
4
5
# cat output/*
hello 1
jjjjj 1
joylau 2
world 1

可以看到每个单词出现的次数

Hadoop 伪分布式环境搭建

我们需要设置 HADOOP 环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
gedit ~/.bashrc

export HADOOP_HOME=/home/hadoop-2.8.2
export HADOOP_INSTALL=$HADOOP_HOME
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export PATH=$PATH:$HADOOP_HOME/sbin:$HADOOP_HOME/bin

source ~/.bashrc

修改配置文件

core-site.xml

1
2
3
4
5
6
7
8
9
10
11
<configuration>
<property>
<name>hadoop.tmp.dir</name>
<value>file:/home/temp</value>
<description>Abase for other temporary directories.</description>
</property>
<property>
<name>fs.defaultFS</name>
<value>hdfs://localhost:9000</value>
</property>
</configuration>

hdfs-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:/home/temp/hdfs/namenode</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:/home/temp/hdfs/datanode</value>
</property>
</configuration>

配置完成后,执行 NameNode 和 DataNode 的格式化:

1
2
./bin/hdfs namenode -format
./bin/hdfs datanode -format

现在启动 Hadoop 伪分布式服务器

1
2
./sbin/start-dfs.sh 
./sbin/start-yarn.sh

以前版本的命令是

1
./sbin/start-all.sh

jps查看启动是否成功启动

1
2
3
4
5
6
7
jps

5360 Jps
4935 ResourceManager
5225 NodeManager
4494 NameNode
4782 SecondaryNameNode

成功启动后,可以访问 Web 界面 http://localhost:50070 查看 NameNode 和 Datanode 信息,还可以在线查看 HDFS 中的文件
运行 stop-all.sh 来关闭所有进程

伪分布式环境实例运行

上面实例的运行时单机版的,伪分布式的实例的运行的不同之处在于,读取文件是在 HDFS 上的

按照常规的尿性,我们先创建个用户目录 ,以后就可以以相对目录来进行文件的操作了

这里得说下 hdfs 的常用 shell

  • 创建目录 ./bin/hdfs dfs -mkdir -p /user/root
  • 上传文档 ./bin/hdfs dfs -put ./input/test.txt input
  • 删除文档 ./bin/hdfs dfs -rmr input
  • 产看文档 ./bin/hdfs dfs -cat input/*
  • 查看列表 ./bin/hdfs dfs -ls input
  • 拉取文档 ./bin/hdfs dfs -get output/* ./output

有了这些简单的命令,现在就可以运行实例

先创建用户目录 ./bin/hdfs dfs -mkdir -p /user/root
在新建一个目录 ./bin/hdfs dfs -mkdir input
将之前的文件上传 ./bin/hdfs dfs -put ./input/test.txt input
上传成功后还可以查看下时候有文件 ./bin/hdfs dfs -ls input
运行实例 ./bin/hadoop jar ./share/hadoop/mapreduce/hadoop-mapreduce-examples-2.8.2.jar wordcount input/ output/
查看运行结果 ./bin/hdfs dfs -cat output/*

其实这些命令都是类 linux 命令,熟悉 linux 命令,这些都很好操作

可以看到统计结果和单机版是一致的

将结果导出 ./bin/hdfs dfs -get output ./output

其实 在 http://host:50070/explorer.html#/user/root 可以看到上传和输出的文件目录

YARN 启动

伪分布式不启动 YARN 也可以,一般不会影响程序执行
YARN 是从 MapReduce 中分离出来的,负责资源管理与任务调度。YARN 运行于 MapReduce 之上,提供了高可用性、高扩展性

首先修改配置文件 mapred-site.xml,这边需要先进行重命名:

1
mv ./etc/hadoop/mapred-site.xml.template ./etc/hadoop/mapred-site.xml

增加配置

1
2
3
4
5
6
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>

配置文件 yarn-site.xml:

1
2
3
4
5
6
<configuration>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configuration>
1
2
./sbin/start-yarn.sh      $ 启动YARN
./sbin/mr-jobhistory-daemon.sh start historyserver # 开启历史服务器,才能在Web中查看任务运行情况

启动 YARN 之后,运行实例的方法还是一样的,仅仅是资源管理方式、任务调度不同。
观察日志信息可以发现,不启用 YARN 时,是 “mapred.LocalJobRunner” 在跑任务,
启用 YARN 之后,是 “mapred.YARNRunner” 在跑任务。
启动 YARN 有个好处是可以通过 Web 界面查看任务的运行情况:http://localhost:8088/cluster

踩坑记录

  • 内存不足:一开始虚拟机只开了2G 内存,出现了很多错误,后来将虚拟机内存开到8G, 就没有问题了
  • hosts 配置,一开始启动的时候会报不识别 localhost 的域名的错误,更改下 hosts文件即可,加一行
1
127.0.0.1   localhost HostName
  • put 上传文档时报错:There are 0 datanode(s) running and no node(s) are excluded in this operation
    这可能由于之前在目录下有操作,有一些其他的文档,只要清空指定的目录,然后再格式化 namenode 和 datanode 就可以了

参考资料

《Hadoop 权威指南 : 第四版》 –Tom White 著

感受

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

首先

首先记录,在这篇文章书写前,自己并不是刚刚上手 Hadoop, 其实学了有一段时间了
在这段时间内,由最开始的对 Hadoop 的懵懂无知到渐渐的熟悉 Hadoop 大致的开发流程
整个过程越来越清晰
于是就想着,把自己接下来在 Hadoop 上的学习计划记录下来

流程

  1. 了解 Hadoop 背景,开发作用
  2. 然后搭建Hadoop集群,先让它在自己电脑上运行。
  3. 学习分布式文件系统HDFS。
  4. 学习分布式计算框架MapReduce
  5. 学习流式计算Storm
  6. 学习分布式协作服务Zookeeper
  7. 学习Hive—数据仓库工具
  8. 学习Hbase—分布式存储系统
  9. 学习Spark
  10. 学习Scala
  11. 学习Spark开发技术

最后

这些技术在工作中是远远不够的,但也不是工作中每项都有用到了
就自己现在公司的大数据环境来说,还有像 impala,zookeeper,spark,kafka…等等
等有新的学习计划再补充吧

0%