开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot-Oauth20(二)—— clien

发表于 2020-09-01

前言

上一篇我们已经用最简单的方式,搭建了一个授权方式是 client_credentials 的 Oauth2 的流程。那么现在,在此基础上,我们就再往前迈一步,我们把 client 信息和 token 存储到数据库当中,方便我们管理。并且密码需要保证安全,那么就需要加密。目标明确,那我们开始动手吧!

client&token 存储到数据库

步骤:

  1. 数据库准备:只创建我们需要用到的表
  2. 添加数据库相关依赖:使用 mysql 数据库
  3. Oauth 存储配置的设置
  4. 验证

数据库准备

在本地数据库,创建两张表:

  • 一张表存储 client 相关信息
  • 另一张表存储 token
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
sql复制代码# client 相关信息
create table oauth_client_details
(
client_id VARCHAR(256) PRIMARY KEY comment '必填,Oauth2 client_id',
resource_ids VARCHAR(256) comment '可选,资源id集合,多个资源用英文逗号隔开',
client_secret VARCHAR(256) comment '必填,Oauth2 client_secret',
scope VARCHAR(256) comment '必填,Oauth2 权限范围,比如 read,write等可自定义',
authorized_grant_types VARCHAR(256) comment '必填,Oauth2 授权类型,支持类型:authorization_code,password,refresh_token,implicit,client_credentials,多个用英文逗号隔开',
web_server_redirect_uri VARCHAR(256) comment '可选,客户端的重定向URI,当grant_type为authorization_code或implicit时,此字段是需要的',
authorities VARCHAR(256) comment '可选,指定客户端所拥有的Spring Security的权限值',
access_token_validity INTEGER comment '可选,access_token的有效时间值(单位:秒),不填写框架(类refreshTokenValiditySeconds)默认12小时',
refresh_token_validity INTEGER comment '可选,refresh_token的有效时间值(单位:秒),不填写框架(类refreshTokenValiditySeconds)默认30天',
additional_information VARCHAR(4096) comment '预留字段,格式必须是json',
autoapprove VARCHAR(256) comment '该字段适用于grant_type="authorization_code"的情况下,用户是否自动approve操作'
);

# token 存储
create table oauth_access_token
(
token_id VARCHAR(256) comment 'MD5加密后存储的access_token',
token BLOB comment 'access_token序列化的二进制数据格式',
authentication_id VARCHAR(256) PRIMARY KEY comment '主键,其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的,具体实现参见DefaultAuthenticationKeyGenerator',
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB comment '将OAuth2Authentication对象序列化后的二进制数据',
refresh_token VARCHAR(256) comment 'refresh_token的MD5加密后的数据'
);

之后,我们需要添加自定义 client 信息。本次演示添加的 client 信息如下(依旧本着尽量能不配置就不配置的原则):

1
sql复制代码INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('gold', 'res', '{noop}123456', 'write', 'client_credentials', null, null, null, null, null, null);

说明

  1. 在官方给出的源码当中,是有对应的 schema.sql 文件,这里只创建涉及我们示例中需要的表
  2. 对两张表的操作,我们可以去看这两个类:JdbcClientDetailsService & JdbcTokenStore
  3. 此外本示例,添加了 resource_ids ,注意在配置 ResourceServerSecurityConfigurer 中对应

Pom

1
2
3
4
5
6
7
8
9
xml复制代码    <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

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

认证服务器代码修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

private final DataSource dataSource;

public MyAuthorizationServerConfigurer(DataSource dataSource) {
this.dataSource = dataSource;
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new JdbcTokenStore(dataSource));
}
}

上述代码只显示改动的部分,修改 client 的配置和 tokenStore 的配置,都进行添加数据源即可

请求 token

下面的请求是在 postman 中进行的。 base_url 为设置的全局变量,实际为 http://127.0.0.1:8080

请求token

获取资源

获取资源

观察数据库

token存储

由于 token 在数据库是存储的是二进制形式,但是我们通过 client_id 数据,可以看出是我们刚刚请求的 client。

到此为止我们已经是实现了,把 client 及 token 信息存储到数据库了,这样更便于我们对 client 及 token 数据的管理。但是数据库存储明文密码是不安全,那么接下来,我们对 client_secret 进行加密。

client_secret 加密

配置 passwordEncoder

SpringBoot Oauth 本身支持的加密算法有很多种,详细信息可以看类 PasswordEncoderFactories ,包括我们最常用的 MD5、SHA 等,我们使用 bcrypt 加密算法, 那么直接配置支持全部算法的 passwordEncoder ,即 DelegatingPasswordEncoder 。

1
2
3
4
5
java复制代码@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());

}

加密 client_secret

既然我们这次使用 bcrypt 加密, 直接可以找到 BCryptPasswordEncoder ,通过它可以给我们的密码进行加密,并把密码存储于数据库当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PasswordEncodeUtil {

private static final BCryptPasswordEncoder bcryptEncoder = new BCryptPasswordEncoder();

public static String bcryptEncode(String password) {
return bcryptEncoder.encode(password);
}

public static String genOauthEncodePwd(String password) {
return "{bcrypt}" + bcryptEncode(password);
}


public static void main(String[] args) {
String oriPwd = "123456";
System.out.println(genOauthEncodePwd(oriPwd));

}
}

原密码:123456

加密后密码:{bcrypt}2a2a2a10$NPxtsEUMmBGTlzVXlT.scubSCXNEDlBAq2r2t7iQFB/.RaNBlh0nO

client_secret加密

注意:加密密码的前缀 大括号 “{xxx}”,是指定加密算法名称。因为框架支持多种加密算法,那么必须需要带有指定加密算法前缀。

请求获取 token

下面的请求是在 postman 中进行的。 base_url 为设置的全局变量,实际为 http://127.0.0.1:8080

请求token

请求资源

请求资源

小结

本文主要是以 client_credentials 的授权方式,把 client 和 token 信息存储在数据库当中,来方便我们管理。同时,为了保证密码的安全,我们把 client_secret 用 bcrypt 算法进行了加密操作,并存储到数据库当中。有了上一篇基础,这篇整体看下来挺简单的吧。那同学们动起来,实现一下吧!

个人水平有限,欢迎大家指正,一起交流哦~

demo:github.com/goldpumpkin…

Reference:

  1. spring-oauth-server 数据库表说明

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

玩转SpringBoot之整合 shiro 权限框架

发表于 2020-09-01

先看再点赞,给自己一点思考的时间,思考过后请直接微信搜索【Java学习之道 】,关注他。对文章有建议的,也欢迎相互交流,我微信:studyjava

在实际项目中,经常需要用到角色权限区分,以此来为不同的角色赋予不同的权利,分配不同的任务。比如,普通用户只能浏览;会员可以浏览和评论;超级会员可以浏览、评论和看视频课等;实际应用场景很多。毫不夸张的说,几乎每个完整的项目都会设计到权限管理。

因此,这篇文章,阿淼就带大家将 shiro 权限框架整合到 SpringBoot 中,以达到快速的实现权限管理的功能。

序

在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是由于 Spring Security 过于庞大和复杂,只要能满足业务需要,大多数公司还是会选择 Apache Shiro 来使用。

一般来说,Spring Security 和 Shiro 的区别如下:

Spring Security Apache Shiro
重量级的安全管理框架 轻量级的安全管理框架
概念复杂,配置繁琐 概念简单、配置简单
功能强大 功能简单

这篇文章会首先带大家了解 Apache Shiro ,然后再给出使用案例 Demo。

走进 Apache Shiro

官网认知

照例又去官网扒了扒介绍:

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro™是一个强大且易用的Java安全框架,能够用于身份验证、授权、加密和会话管理。Shiro拥有易于理解的API,您可以快速、轻松地获得任何应用程序——从最小的移动应用程序到最大的网络和企业应用程序。

简而言之,Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。

Shiro能到底能做些什么呢?

  • 验证用户身份
  • 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
  • 在非 Web 或 EJB 容器的环境下可以任意使用Session API
  • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
  • 支持单点登录(SSO)功能
  • 支持提供“Remember Me”服务,获取用户关联信息而无需登录
    ···

为什么今天还要使用Apache Shiro?

对此,官方给出了详细的解释:shiro.apache.org/

自2003年以来,框架环境发生了很大变化,因此今天仍然有充分的理由使用Shiro。实际上有很多原因。Apache Shiro是:

  • 易于使用 -易于使用是该项目的最终目标。应用程序安全性可能非常令人困惑和沮丧,并被视为“必要的邪恶”。如果您使它易于使用,以使新手程序员可以开始使用它,那么就不必再痛苦了。
  • 全面 -Apache Shiro声称没有其他具有范围广度的安全框架,因此它可能是满足安全需求的“一站式服务”。
  • 灵活 -Apache Shiro可以在任何应用程序环境中工作。尽管它可以在Web,EJB和IoC环境中运行,但并不需要它们。Shiro也不要求任何规范,甚至没有很多依赖性。
  • 具有Web功能 -Apache Shiro具有出色的Web应用程序支持,使您可以基于应用程序URL和Web协议(例如REST)创建灵活的安全策略,同时还提供一组JSP库来控制页面输出。
  • 可插拔 -Shiro干净的API和设计模式使它易于与许多其他框架和应用程序集成。您会看到Shiro与Spring,Grails,Wicket,Tapestry,Mule,Apache Camel,Vaadin等框架无缝集成。
  • 受支持 -Apache Shiro是Apache Software Foundation(Apache软件基金会)的一部分,该组织被证明以其社区的最大利益行事。项目开发和用户群体友好的公民随时可以提供帮助。如果需要,像Katasoft这样的商业公司也可以提供专业的支持和服务。

Shiro 核心概念

Apache Shiro 是一个全面的、蕴含丰富功能的安全框架。

下图为描述 Shiro 功能的框架图:

图片来源于网络

如图所示,功能包括:

  • Authentication(认证):用户身份识别,通常被称为用户“登录”
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
  • Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。

并且 Shiro 还有通过增加其他的功能来支持和加强这些不同应用环境下安全领域的关注点。

特别是对以下的功能支持:

  • Web支持:Shiro 提供的 Web 支持 api ,可以很轻松的保护 Web 应用程序的安全。
  • 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
  • 并发:Apache Shiro 支持多线程应用程序的并发特性。
  • 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
  • “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
  • “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。

注意: Shiro 不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给 Shiro

使用案例 Demo

1.新建 maven 项目

为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站。

  • 1、打开浏览器,访问:start.spring.io/
  • 2、根据页面提示,选择构建工具,开发语言,项目信息等。

2.导入 springboot 父依赖

1
2
3
4
5
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>

3.相关 jar 包

web 包

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

shiro-spring 包就是此篇文章的核心

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
xml复制代码<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
shiro 注解会用到 aop
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
数据库相关包使用的是mybatisplus
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>

4.数据库

建表语句在项目中有,项目地址: github.com/mmzsblog/mm…

5.自定义 realm

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
java复制代码public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// HttpServletRequest request = (HttpServletRequest) ((WebSubject) SecurityUtils
// .getSubject()).getServletRequest();//这个可以用来获取在登录的时候提交的其他额外的参数信息
String username = (String) principals.getPrimaryPrincipal();
// 受理权限
// 角色
Set<String> roles = new HashSet<String>();
Role role = roleService.getRoleByUserName(username);
System.out.println(role.getRoleName());
roles.add(role.getRoleName());
authorizationInfo.setRoles(roles);
// 权限
Set<String> permissions = new HashSet<String>();
List<Permission> querypermissions = permissionService.getPermissionsByRoleId(role.getId());
for (Permission permission : querypermissions) {
permissions.add(permission.getPermissionName());
}
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
String loginName = (String) authcToken.getPrincipal();
// 获取用户密码
User user = userService.getOne(new QueryWrapper<User>().eq("username", loginName));
if (user == null) {
// 没找到帐号
throw new UnknownAccountException();
}
String password = new String((char[]) authcToken.getCredentials());
String inpass = (new Md5Hash(password, user.getUsername())).toString();
if (!user.getPassword().equals(inpass)) {
throw new IncorrectCredentialsException();
}
// 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(loginName, user.getPassword(),
ByteSource.Util.bytes(loginName), getName());

return authenticationInfo;
}

}

6.shiro 配置类

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
java复制代码@Configuration
public class ShiroConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);

/**
* Shiro的Web过滤器Factory 命名:shiroFilter
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
//需要权限的请求,如果没有登录则会跳转到这里设置的url
shiroFilterFactoryBean.setLoginUrl("/login.html");
//设置登录成功跳转url,一般在登录成功后自己代码设置跳转url,此处基本没用
shiroFilterFactoryBean.setSuccessUrl("/main.html");
//设置无权限跳转界面,此处一般不生效,一般自定义异常
shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
Map<String, Filter> filterMap = new LinkedHashMap<>();
// filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
/*
* 定义shiro过滤链 Map结构
* Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
* anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
* authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.
* FormAuthenticationFilter
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/*
* 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边; authc:所有url都必须认证通过才可以访问;
* anon:所有url都都可以匿名访问
*/
filterChainDefinitionMap.put("/login.html", "authc");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

/**
* 权限管理
*/
@Bean
public SecurityManager securityManager() {
logger.info("=======================shiro=======================");
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(MyShiroRealm());
// securityManager.setRememberMeManager(rememberMeManager);
return securityManager;
}

/**
* Shiro Realm 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的
*/
@Bean
public MyShiroRealm MyShiroRealm() {
MyShiroRealm userRealm = new MyShiroRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}

/**
* 凭证匹配器 密码验证
*/
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(1);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}

/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}

}

7.测试类

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
java复制代码@RestController
public class UserController {
@PostMapping("login")
public String name(String username, String password) {
String result = "已登录";
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (!currentUser.isAuthenticated()) {
try {
currentUser.login(token);// 会触发com.shiro.config.MyShiroRealm的doGetAuthenticationInfo方法
result = "登录成功";
} catch (UnknownAccountException e) {
result = "用户名错误";
} catch (IncorrectCredentialsException e) {
result = "密码错误";
}
}
return result;
}

@GetMapping("logout")
public void logout() {
Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
}

@RequiresPermissions("role:update")
@GetMapping("/role")
public String name() {
return "hello";
}

@RequiresPermissions("user:select")
@GetMapping("/role2")
public String permission() {
return "hello sel";
}

}

7.1 登录测试

数据库账号(密码经过md5加盐加密)

数据库账号

账号错误测试

密码错误测试

账号正确测试

登录成功界面

7.2 权限测试

权限测试1

权限测试2

8.说明

8.1 无权限时的处理

无权限时自定义了一个异常。所以,权限测试的时候没有权限就会提示配置的提示语 “没有权限”。

1
2
3
4
5
6
7
8
java复制代码@ControllerAdvice
public class ShiroException {
@ExceptionHandler(value = UnauthorizedException.class)
@ResponseBody
public String name() {
return "没有权限";
}
}

8.2 角色权限测试与权限测试相同

权限设置可在shiro配置类中shiro过滤链设置,也可用注解方式设置,本文使用注解方式。

8.3 shiro 的 session 和 cache

shiro 的 session 和 cache 管理可以自定义,本文用的是默认的,推荐自定义,方便管理。

小结

  • Apache Shiro是Java的一个安全框架
  • Shiro是一个强大的简单易用的Java安全框架,主要用来更便捷的认证、授权、加密、会话管理、与Web集成、缓存等
  • Shiro使用起来小而简单
  • spring中有spring security ,是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
  • shiro不依赖于spring,shiro不仅可以实现web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,
  • shiro属于轻量框架,越来越多企业项目开始使用shiro.

参考:www.cnblogs.com/joker-dj/ar…

我是阿淼,你的 【三连】 就是阿淼创作的最大动力,如果本篇博客有任何错误和建议,欢迎大家留言!

文章持续更新,可以微信搜索「 Java学习之道 」第一时间阅读,回复【666】有我准备的程序员必备电子书 + 超多高清教学视频,以及突击面试题整理,欢迎来取。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

有了这个神器,轻松用 Python 写 APP !

发表于 2020-08-31

本文转自机器之心,作者:Adrien Treuille,机器之心编译,参与:魔王、一鸣,如有侵权,则可删除。

机器学习开发者想要打造一款 App 有多难?事实上,你只需要会 Python 代码就可以了,剩下的工作都可以交给一个工具。近日,Streamlit 联合创始人 Adrien Treuille 撰文介绍其开发的机器学习工具开发框架——Streamlit,这是一款专为机器学习工程师创建的免费、开源 app 构建框架。这款工具可以在你写 Python 代码的时候,实时更新你的应用。目前,Streamlit 的 GitHub Star 量已经超过 3400,在 medim 上的热度更是达到了 9000+。

Streamlit 网站:https://streamlit.io/
GitHub地址:https://github.com/streamlit/streamlit/

用 300 行 Python 代码,编程一个可实时执行神经网络推断的语义搜索引擎。

以我的经验,每一个不平凡的机器学习项目都是用错误百出、难以维护的内部工具整合而成的。这些工具通常用 Jupyter Notebooks 和 Flask app 写成,很难部署,需要对客户端服务器架构(C/S 架构)进行推理,且无法与 Tensorflow GPU 会话等机器学习组件进行很好的整合。

我第一次看到此类工具是在卡内基梅隆大学,之后又在伯克利、Google X、Zoox 看到。这些工具最初只是小的 Jupyter notebook:传感器校准工具、仿真对比 app、激光雷达对齐 app、场景重现工具等。

当一个工具越来越重要时,项目经理会介入其中:进程和需求不断增加。这些单独的项目变成代码脚本,并逐渐发展成为冗长的「维护噩梦」……

机器学习工程师创建 app 的流程(ad-hoc)。

而当一个工具非常关键时,我们会组建工具团队。他们熟练地写 Vue 和 React,在笔记本电脑上贴满声明式框架的贴纸。他们的设计流程是这样式的:

工具团队构建 app 的流程(干净整洁,从零开始)。

这简直太棒了!但是所有这些工具都需要新功能,比如每周上线新功能。然而工具团队可能同时支持 10 多个项目,他们会说:「我们会在两个月内更新您的工具。」

我们返回之前自行构建工具的流程:部署 Flask app,写 HTML、CSS 和 JavaScript,尝试对从 notebook 到样式表的所有一些进行版本控制。我和在 Google X 工作的朋友 Thiago Teixeira 开始思考:如果构建工具像写 Python 脚本一样简单呢?

我们希望在没有工具团队的情况下,机器学习工程师也能构建不错的 app。这些内部工具应该像机器学习工作流程的副产品那样自然而然地出现。写此类工具感觉就像训练神经网络或者在 Jupyter 中执行点对点分析(ad-hoc analysis)!同时,我们还想保留强大 app 框架的灵活性。我们想创造出令工程师骄傲的好工具。

我们希望的 app 构建流程如下:

Streamlit app 构建流程。

与来自 Uber、Twitter、Stitch Fix、Dropbox 等的工程师一道,我们用一年时间创造了 Streamlit,这是一个针对机器学习工程师的免费开源 app 框架。不管对于任何原型,Streamlit 的核心原则都是更简单、更纯粹。

Streamlit 的核心原则如下:

  1. 拥抱 Python

Streamlit app 是完全自上而下运行的脚本,没有隐藏状态。你可以利用函数调用来处理代码。只要你会写 Python 脚本,你就可以写 Streamlit app。例如,你可以按照以下代码对屏幕执行写入操作:

1
javascript复制代码import streamlit as stst.write('Hello, world!')

  1. 把 widget 视作变量

Streamlit 中没有 callback!每一次交互都只是自上而下重新运行脚本。该方法使得代码非常干净:

1
2
java复制代码import streamlit as stx = st.slider('x')
st.write(x, 'squared is', x * x)

3 行代码写成的 Streamlit 交互 app。

  1. 重用数据和计算

如果要下载大量数据或执行复杂计算,怎么办?关键在于在多次运行中安全地重用信息。Streamlit 引入了 cache primitive,它像一个持续的默认不可更改的数据存储器,保障 Streamlit app 轻松安全地重用信息。例如,以下代码只从 Udacity 自动驾驶项目(github.com/udacity/sel… app:

*使用 st.cache,在 Streamlit 多次运行中保存数据。代码运行说明,参见:gist.github.com/treuille/c6…

运行以上 st.cache 示例的输出。

简而言之,Streamlit 的工作流程如下:

  1. 每次用户交互均需要从头运行全部脚本。
  2. Streamlit 根据 widget 状态为每个变量分配最新值。
  3. 缓存保证 Streamlit 重用数据和计算。

如下图所示:

用户事件触发 Streamlit 从头开始重新运行脚本。不同运行中仅保留缓存。

感兴趣的话,你可以立刻尝试!只需运行以下行:

网页浏览器将自动打开,并转向本地 Streamlit app。如果没有出现浏览器窗口,只需点击链接。

这些想法很简洁,但有效,使用 Streamlit 不会妨碍你创建丰富有用的 app。我在 Zoox 和 Google X 工作时,看着自动驾驶汽车项目发展成为数 G 的视觉数据,这些数据需要搜索和理解,包括在图像数据上运行模型进而对比性能。我看到的每一个自动驾驶汽车项目都有整支团队在做这方面的工具。

在 Streamlit 中构建此类工具非常简单。以下 Streamlit demo 可以对整个 Udacity 自动驾驶汽车照片数据集执行语义搜索,对人类标注的真值标签进行可视化,并在 app 内实时运行完整的神经网络(YOLO)。

这个 300 行代码写成的 Streamlit demo 结合了语义视觉搜索和交互式神经网络推断。

整个 app 只有 300 行 Python 代码,其中大部分是机器学习代码。事实上,整个 app 里只有 23 次 Streamlit 调用。你可以试试看:

我们与机器学习团队合作,为他们的项目而努力时,逐渐意识到这些简单的想法会带来大量重要的收益:

Streamlit app 是纯 Python 文件。你可以使用自己喜欢的编辑器和 debugger。

我用 Streamlit 构建 app 时喜欢用 VSCode 编辑器(左)和 Chrome(右)。

纯 Python 代码可与 Git 等源码控制软件无缝对接,包括 commits、pull requests、issues 和 comment。由于 Streamlit 的底层语言是 Python,因此你可以免费利用这些协作工具的好处。

Streamlit app 是 Python 脚本,因此你可以使用 Git 轻松执行版本控制。

Streamlit 提供即时模式的编程环境。当 Streamlit 检测出源文件变更时,只需点击 Always rerun 即可。

点击「Always rerun」,保证实时编程。

缓存简化计算流程。一连串缓存函数自动创建出高效的计算流程!你可以尝试以下代码:

*Streamlit 中的简单计算流程。运行以上代码,参见说明:gist.github.com/treuille/ac…

基本上,该流程涉及加载元数据到创建摘要等步骤(load_metadata → create_summary)。该脚本每次运行时,Streamlit 仅需重新计算该流程的子集即可。

为了保证 app 的可执行性,Streamlit 仅计算更新 UI 所必需的部分。

Streamlit 适用于 GPU。Streamlit 可以直接访问机器级原语(如 TensorFlow、PyTorch),并对这些库进行补充。例如,以下 demo 中,Streamlit 的缓存存储了整个英伟达 PGGAN。该方法可使用户在更新左侧滑块时,app 执行近乎即时的推断。

该 Streamlit app 使用 TL-GAN 展示了英伟达 PGGAN 的效果。

Streamlit 是免费开源库,而非私有 web app。你可以本地部署 Streamlit app,不用提前联系我们。你甚至可以在不联网的情况下在笔记本电脑上本地运行 Streamlit。此外,现有项目也可以渐进地使用 Streamlit。

渐进地使用 Streamlit 的几种方式。

以上只是 Streamlit 功能的冰山一角而已。它最令人兴奋的一点是,这些原语可以轻松组成复杂 app,但看起来却只是简单脚本。这就要涉及架构运作原理和功能了,本文暂不谈及。

Streamlit 组件图示。

我们很高兴与社区分享 Streamlit,希望它能够帮助大家轻松将 Python 脚本转化为美观实用的机器学习 app。

原文链接:https://towardsdatascience.com/coding-ml-tools-like-you-code-ml-models-ddba3357eace

参考文献:

[1] J. Redmon and A. Farhadi, YOLOv3: An Incremental Improvement (2018), arXiv.

[2] T. Karras, T. Aila, S. Laine, and J. Lehtinen, Progressive Growing of GANs for Improved Quality, Stability, and Variation (2018), ICLR.

[3] S. Guan, Controlled image synthesis and editing using a novel TL-GAN model (2018), Insight Data Science Blog.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Java 路线」 编译过程(编译前端 & 编译后端) 前

发表于 2020-08-31

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 经过前面几篇文章的积累,相信你已经掌握了 静态的 Class 文件的结构,也理解了虚拟机类加载和字节码执行的 动态过程;
  • 这篇文章,我们来聊一聊 Java 的编译过程,你将看到从源码到字节码再到本地代码的整个过程。请点赞,你的点赞和关注真的对我非常重要!

目录


  1. 经典程序编译原理

将源代码翻译为目标代码的过程,称为编译过程,经典的程序编译过程包含以下过程:

经典编译原理 示意图

  • 如果将目标代码理解为中间代码,就是狭义上的编译过程。例如*.c文件编译生成*.obj文件的过程,或者*.java文件编译生成*.class文件的过程;
  • 如果将目标代码理解为最终执行的机器代码,那就是广义上的编译过程。那么还包括*.obj文件链接为可执行的*.exe文件的过程,或者*.class解释 / 编译为机器代码的过程;

经典编译原理 示意图

在Java技术下,除非有特定的上下文语境,编译一词通常指的是*.java转换为*.class的过程,这个过程也被称为 编译前端。除此之外,编译一词还可以指运行期即时编译(JIT,Just in Time Compile)或者(静态的)提前编译(AOT,Ahead of Time Compile),这两种编译称为 编译后端。

下面,我们整理一下 编译前端 & 编译后端 的要点:


  1. 编译前端

编译前端阶段中,最重要的一个编译器就是javac 编译器, 在命令行执行javac命令,其实本质是运行了javac.exe这个应用。Android工程师可能对于 Gradle构建过程更为熟悉,构建过程中有一个Task:compileDebugJavaWithJavac,其实也用到了javac 编译器,编译中间产物路径在build/intermediates/javac/debug/classes。

2.1 javac 编译

下面,我们整理javac 编译器的主要处理过程:

javac 编译过程

延伸文章:

  • 关于注解处理器,请阅读:《Java | 注解处理器原理解析与实践》
  • 关于类加载,请阅读:《Java | 谈谈你对类加载过程的理解》
  • 关于方法调用,请阅读:《Java | 深入理解方法调用的本质(含重载与重写区别)》

2.2 Java 常用语法糖

语法糖 指的是高级语言中的某种语法,这些语法糖在编译时进行解语法糖,转换为无糖语法。这些语法糖大多都是靠编译器实现,而不是依赖字节码或者虚拟机的底层支持。下表总结了部分熟知的语法糖:

Java 常见语法糖 示例

延伸文章:

  • 关于泛型,请阅读:《Java | 关于泛型能问的都在这里了(含Kotlin)》
  • 关于内部类,请阅读:《Java | 为什么非静态内部类会持有外部类的引用》
  • 关于switch,请阅读:《Java | switch 和 if-else 哪个更高效》

  1. 编译后端

在编译后端阶段中,最重要的是 运行期即时编译器(JIT,Just in Time Compiler)和 静态的提前编译器(AOT,Ahead of Time Compiler)。

3.1 解释执行 & 编译执行

根据前面的内容,我们知道编译前端的核心编译产物是:Class 文件。但是对于CPU来说,它是不认得字节码的。在《Java | 为什么 Java 实现了平台无关性》这篇文章里,我们强调了:每种CPU只能“读懂”自身支持的机器语言或者本地代码(native code)。

因此,Java 虚拟机在执行字节码时,需要将字节码翻译为当前平台的本地代码,可以分为:解释执行 & 编译执行,具体如下:

需要注意的是,并不是所有的Java 虚拟机都采用解释器与编译器并存的运行架构。当触发即时编译时,执行引擎(默认)不会等待即时编译完成,而是先按解释执行的方式继续执行。直到即时编译完成后,方法的入口地址才会被修改。

3.2 即时编译器热点探测

那么,即时编译器是如何探测热点代码的呢?具体来说,探测的热点代码有两种::被多次调用的方法 & 被多次执行的循环体。使用的探测方法有:基于采样 & 基于计数器:

3.3 编译器优化技术

Editting…


  1. 总结

  • 编译前端的优化措施主要目的是降低程序员的编码复杂度,提高编码效率。语法糖并不是不是依赖字节码或者虚拟机的底层支持,在编译后会转换为基础的无糖语法。另外,注解处理器相当于编译器的插件,开发人员可以通过它对前端编译施加影响。
  • 编译后端的优化措施主要目的是生成更高效的机器代码,提高运行效率。提前编译在程序运行前编译字节码,相当于提前预热。而即时编译器在运行时动态监控程序运行,探测得热点代码后编译为本地代码,生成的代码更高效,但需要预热期。

参考资料

  • 《深入理解Java虚拟机(第3版本)》(第10、11章)—— 周志明 著
  • 《深入理解Android:Java虚拟机 ART》 —— 邓凡平 著
  • 《深入理解 JVM 字节码》(第4、5章)—— 张亚 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:我就问了一个JVM性能调优,没想到他能吹半个小时 一

发表于 2020-08-31

一、JVM内存模型及垃圾收集算法

1.根据Java虚拟机规范,JVM将内存划分为:

  • New(年轻代)
  • Tenured(年老代)
  • 永久代(Perm)

其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,由虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。

  • 年轻代(New):年轻代用来存放JVM刚分配的Java对象
  • 年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代
  • 永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间。

New又分为几个部分:

  • Eden:Eden用来存放JVM刚分配的对象
  • Survivor1
  • Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。

2.垃圾回收算法

垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:

  • Serial算法(单线程)
  • 并行算法
  • 并发算法

JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。

稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。

还有一个问题是,垃圾回收动作何时执行?

  • 当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的是,年轻代满是指Eden代满,Survivor满不会引发GC
  • 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
  • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出

  • JVM98%的时间都花费在内存回收
  • 每次回收的内存小于2%

满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。

二、内存泄漏及解决方法

1.系统崩溃前的一些现象:

  • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
  • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
  • 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

2.生成堆的dump文件

通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

3.分析dump文件

下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:

  1. Visual VM
  2. IBM HeapAnalyzer
  3. JDK 自带的Hprof工具

使用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都无法直观地观察到内存泄漏,Visual VM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。因此,我们又选用了Eclipse专门的静态内存分析工具:Mat。

4.分析内存泄漏

通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。

另,通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

5.回归问题

Q:为什么崩溃前垃圾回收的时间越来越长?

A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

Q:为什么Full GC的次数越来越多?

A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

Q:为什么年老代占用的内存越来越大?

A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

三、性能调优

除了上述内存泄漏外,我们还发现CPU长期不足3%,系统吞吐量不够,针对8core×16G、64bit的Linux服务器来说,是严重的资源浪费。

在CPU负载不足的同时,偶尔会有用户反映请求的时间过长,我们意识到必须对程序及JVM进行调优。从以下几个方面进行:

  • 线程池:解决用户响应时间长的问题
  • 连接池
  • JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
  • 程序算法:改进程序逻辑算法提高性能

1.Java线程池(java.util.concurrent.ThreadPoolExecutor)

大多数JVM6上的应用采用的线程池都是JDK自带的线程池,之所以把成熟的Java线程池进行罗嗦说明,是因为该线程池的行为与我们想象的有点出入。Java线程池有几个重要的配置参数:

  • corePoolSize:核心线程数(最新线程数)
  • maximumPoolSize:最大线程数,超过这个数量的任务会被拒绝,用户可以通过RejectedExecutionHandler接口自定义处理方式
  • keepAliveTime:线程保持活动的时间
  • workQueue:工作队列,存放执行的任务

Java线程池需要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不同选择,线程池有完全不同的行为:

  • SynchronousQueue: 一个无容量的等待队列,一个线程的insert操作必须等待另一线程的remove操作,采用这个Queue线程池将会为每个任务分配一个新线程
  • LinkedBlockingQueue : 无界队列,采用该Queue,线程池将忽略 maximumPoolSize参数,仅用corePoolSize的线程处理所有的任务,未处理的任务便在LinkedBlockingQueue中排队
  • ArrayBlockingQueue: 有界队列,在有界队列和 maximumPoolSize的作用下,程序将很难被调优:更大的Queue和小的maximumPoolSize将导致CPU的低负载;小的Queue和大的池,Queue就没起动应有的作用。

其实我们的要求很简单,希望线程池能跟连接池一样,能设置最小线程数、最大线程数,当最小数<任务<最大数时,应该分配新的线程处理;当任务>最大数时,应该等待有空闲线程再处理该任务。

但线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,如果Queue满且无法派生新线程,就拒绝该任务。设计导致“先放等执行”、“放不下再执行”、“拒绝不等待”。所以,根据不同的Queue参数,要提高吞吐量不能一味地增大maximumPoolSize。

当然,要达到我们的目标,必须对线程池进行一定的封装,幸运的是ThreadPoolExecutor中留了足够的自定义接口以帮助我们达到目标。我们封装的方式是:

  • 以SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配,同时可以通过提高maximumPoolSize来提高系统吞吐量
  • 自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。

2.连接池(org.apache.commons.dbcp.BasicDataSource)

在使用org.apache.commons.dbcp.BasicDataSource的时候,因为之前采用了默认配置,所以当访问量大时,通过JMX观察到很多Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,直接原因当时是因为BasicDataSource连接池的最大连接数设置的太小,默认的BasicDataSource配置,仅使用8个最大连接。

我还观察到一个问题,当较长的时间不访问系统,比如2天,DB上的Mysql会断掉所有的连接,导致连接池中缓存的连接不能用。为了解决这些问题,我们充分研究了BasicDataSource,发现了一些优化的点:

  • Mysql默认支持100个链接,所以每个连接池的配置要根据集群中的机器数进行,如有2台服务器,可每个设置为60
  • initialSize:参数是一直打开的连接数
  • minEvictableIdleTimeMillis:该参数设置每个连接的空闲时间,超过这个时间连接将被关闭
  • timeBetweenEvictionRunsMillis:后台线程的运行周期,用来检测过期连接
  • maxActive:最大能分配的连接数
  • maxIdle:最大空闲数,当连接使用完毕后发现连接数大于maxIdle,连接将被直接关闭。只有initialSize < x < maxIdle的连接将被定期检测是否超期。这个参数主要用来在峰值访问时提高吞吐量。
  • initialSize是如何保持的?经过研究代码发现,BasicDataSource会关闭所有超期的连接,然后再打开initialSize数量的连接,这个特性与minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一起保证了所有超期的initialSize连接都会被重新连接,从而避免了Mysql长时间无动作会断掉连接的问题。

3.JVM参数

在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:

  • GC的时间足够的小
  • GC的次数足够的少
  • 发生Full GC的周期足够的长

前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

(1)针对JVM堆的设置一般,可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值

(2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小

(3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响

  • 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
  • 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
  • 如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间

(4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集

(5)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

(4)可以通过下面的参数打Heap Dump信息

  • -XX:HeapDumpPath
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:/usr/aaa/dump/heap_trace.txt

通过下面参数可以控制OutOfMemoryError时打印堆的信息

  • -XX:+HeapDumpOnOutOfMemoryError

请看一下一个时间的Java参数配置:(服务器:Linux 64Bit,8Core×16G)

JAVA_OPTS=”$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G”

经过观察该配置非常稳定,每次普通GC的时间在10ms左右,Full GC基本不发生,或隔很长很长的时间才发生一次

通过分析dump文件可以发现,每隔1小时都会发生一次Full GC,经过多方求证,只要在JVM中开启了JMX服务,JMX将会1小时执行一次Full GC以清除引用,关于这点请参考附件文档。

4.程序算法调优

最后

感谢大家看到这里,文章有不足,欢迎大家指出;如果你觉的写得不错,欢迎转发与点赞!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

没想到 Springboot + Flowable 开发工作

发表于 2020-08-31

本文收录在个人博客:www.chengxy-nds.top,技术资料共享,同进步

程序员是块砖,哪里需要哪里搬

公司内部的OA系统最近要升级改造,由于人手不够就把我借调过去了,但说真的我还没做过这方面的功能,第一次接触工作流的开发,还是有点好奇是个怎样的流程。

项目主要用 Springboot + Flowable 重构原有的工作流程,Flowable 是个用 Java语言写的轻量级工作流引擎,上手比较简单开发效率也挺高的,一起学习下这个框架。

官方地址:https://www.flowable.org/docs/userguide/index.html,分享的只是简单应用,深入研究还得看官方文档。

Flowable 核心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
javascript复制代码<!--flowable工作流依赖-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

流程设计

工作流开发的核心是任务流程的设计,Flowable 官方建议采用业界标准BPMN2.0的 XML来描述需要定义的工作流。

我们需要在 resource 目录下创建 processes路径,存放相关的 XML流程配置文件。Flowable 框架会默认加载此目录下的工作流文件并解析 XML,并将解析后的流程配置信息持久化到数据库。

Flowable 是依赖于数据库的,但它并不需要我们手动的创建表,而是在程序第一次启动时,自动的向MySQL 中创建它所需要的一系列表。

1
2
3
4
5
javascript复制代码spring:
datasource:
url: jdbc:mysql://47.93.6.5:3306/order?serverTimezone=UTC
username: root
password: 123455

看到项目启动成功一共生成了60个表,数量还是比较多的,建议使用专门的数据库存在这些工作流表。

举个栗子:假如一个请假流程,需要经理审核通过,请假才能生效,如果他驳回流程结束。

接下来我们用 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
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="Leave" name="LeaveProcess" isExecutable="true">
<userTask id="leaveTask" name="请假" flowable:assignee="${leaveTask}"/>
<userTask id="managerTask" name="经理审核"/>
<exclusiveGateway id="managerJudgeTask"/>
<endEvent id="endLeave" name="结束"/>
<startEvent id="startLeave" name="开始"/>
<sequenceFlow id="modeFlow" sourceRef="leaveTask" targetRef="managerTask"/>
<sequenceFlow id="flowStart" sourceRef="startLeave" targetRef="leaveTask"/>
<sequenceFlow id="jugdeFlow" sourceRef="managerTask" targetRef="managerJudgeTask"/>
<endEvent id="endLeave2"/>
<sequenceFlow id="flowEnd" name="通过" sourceRef="managerJudgeTask" targetRef="endLeave">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${checkResult=='通过'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="rejectFlow" name="驳回" sourceRef="managerJudgeTask"
targetRef="endLeave2">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${checkResult=='驳回'}]]>
</conditionExpression>
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_process">
<bpmndi:BPMNPlane bpmnElement="Leave" id="BPMNPlane_process">
<bpmndi:BPMNShape bpmnElement="leaveTask" id="BPMNShape_leaveTask">
<omgdc:Bounds height="79.99999999999999" width="100.0" x="304.60807973558974" y="122.00000000000001"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="managerTask" id="BPMNShape_managerTask">
<omgdc:Bounds height="80.0" width="100.0" x="465.0" y="122.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="managerJudgeTask" id="BPMNShape_managerJudgeTask">
<omgdc:Bounds height="40.0" width="40.0" x="611.5" y="142.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endLeave" id="BPMNShape_endLeave">
<omgdc:Bounds height="28.0" width="28.0" x="696.5" y="148.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="startLeave" id="BPMNShape_startLeave">
<omgdc:Bounds height="30.0" width="30.0" x="213.2256558149128" y="147.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endLeave2"
id="BPMNShape_endLeave2">
<omgdc:Bounds height="28.0" width="28.0" x="617.5" y="73.32098285753572"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flowEnd" id="BPMNEdge_flowEnd">
<omgdi:waypoint x="651.1217948717949" y="162.37820512820514"/>
<omgdi:waypoint x="696.5002839785394" y="162.0891701657418"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="rejectFlow" id="BPMNEdge_rejectFlow">
<omgdi:waypoint x="631.866093577786" y="142.36609357778607" />
<omgdi:waypoint x="631.5931090276993" y="101.32067323657485" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="modeFlow" id="BPMNEdge_modeFlow">
<omgdi:waypoint x="404.60807973558974" y="162.0" />
<omgdi:waypoint x="465.0" y="162.0" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flowStart" id="BPMNEdge_flowStart">
<omgdi:waypoint x="243.2256558149128" y="162.0" />
<omgdi:waypoint x="304.60807973558974" y="162.0" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="jugdeFlow" id="BPMNEdge_jugdeFlow">
<omgdi:waypoint x="565.0" y="162.21367521367523" />
<omgdi:waypoint x="611.9141630901288" y="162.41416309012877" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

其实就是把流程图的各种线条逻辑,用不同的XML标签描绘出来了。

<process> : 表示一个完整的工作流

<documentation> : 对工作流的描述

<startEvent> : 工作流中起点位置(开始)

<endEvent > : 工作流中结束位置(结束)

<userTask> : 代表一个任务审核节点(组长、经理等角色)

<exclusiveGateway> : 逻辑判断节点,相当于流程图中的菱形框

<sequenceFlow> :链接各个节点的线条,sourceRef 属性表示线的起始节点,targetRef 属性表示线指向的节点。

上边这一大坨XML是不是看着超级麻烦,要是有自动生成工具就好了,我发现IDEA自带设计工具,但实在是太难用了。

作为一个面向百度编程的程序员,别的不行上网找答案的能力还是可以的,既然我都觉得写XML麻烦,那么想来官方肯定也想到了,说不定有现成的工具,逛了一圈官网https://www.flowable.org/downloads.html ,居然真的找到了。

github下载地址:github.com/flowable/fl……….


又找了个在线编辑的工具: www.learun.cn:8090/home_online…

流程审批

流程设计完后剩下的就是对工作流的审批和生成流程图。

首先启动一个请假的流程,以员工ID staffId 作为唯一标识,XML文件中会接收变量 leaveTask,Flowable内部会进行数据库持久化,并返回一个流程Id processId ,用它可以查询工作流的整体情况,任务Id task为员工具体的请假任务。

注意:一个请假流程 processId中可以包含多个请假任务 taskId。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码/**
* @author xiaofu
* @description 启动流程
* @date 2020/8/26 17:36
*/
@RequestMapping(value = "startLeaveProcess")
@ResponseBody
public String startLeaveProcess(String staffId) {
HashMap<String, Object> map = new HashMap<>();
map.put("leaveTask", staffId);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("Leave", map);
StringBuilder sb = new StringBuilder();
sb.append("创建请假流程 processId:" + processInstance.getId());
List<Task> tasks = taskService.createTaskQuery().taskAssignee(staffId).orderByTaskCreateTime().desc().list();
for (Task task : tasks) {
sb.append("任务taskId:" + task.getId());
}
return sb.toString();
}

用启动流程时返回的 processId 看一下一下当前的流程图

1
java复制代码http://localhost:4000/leave/createProcessDiagramPic?processId=37513

接下来将请假申请进行驳回 ,传入相应的 taskId 后执行驳回,再看看整个工作流的效果。

1
javascript复制代码http://localhost:4000/leave/rejectTask?taskId=10086
1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码 /**
* @param taskId
* @author xinzhifu
* @description 驳回
* @date 2020/8/27 14:30
*/
@ResponseBody
@RequestMapping(value = "rejectTask")
public String rejectTask(String taskId) {
HashMap<String, Object> map = new HashMap<>();
map.put("checkResult", "驳回");
taskService.complete(taskId, map);
return "申请审核驳回~";
}

看到整个请假流程在经理审核这成功阻断了。

1
javascript复制代码http://localhost:4000/leave/createProcessDiagramPic?processId=37513

总结

开发工作流一般多用在OA系统等传统项目中,我也是第一次尝试做此类功能,收获还是蛮多的,技术栈又压进了一个知识点。今天分享的是个超级简单的demo,因为也是刚开始接触,等我用的贼溜的时候,再给小伙伴们做更成熟更深入的分享。

demo的github 地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-work-flowable


原创不易,燃烧秀发输出内容,如果有一丢丢收获,点个赞鼓励一下吧!

整理了几百本各类技术电子书,送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试:为了进阿里,又把并发CAS(Compare and S

发表于 2020-08-30

前言

在面试中,并发线程安全提问必然是不会缺少的,那基础的CAS原理也必须了解,这样在面试中才能加分,那来看看面试可能会问那些问题:

  • 什么是乐观锁与悲观锁
  • 什么乐观锁的实现方式-CAS(Compare and Swap),CAS(Compare and Swap)实现原理
  • 在JDK并发包中的使用
  • CAS的缺陷

1. 什么是乐观锁与悲观锁?

悲观锁

总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

  • 传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;
  • Java里面的同步synchronized关键字的实现。

乐观锁

乐观锁,其实就是一种思想,总是认为不会产生并发问题,每次读取数据的时候都认为其他线程不会修改数据,所以不上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。实现方式:

  • CAS实现:Java中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种CAS实现方式,CAS分析看下节。
  • 版本号控制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功

乐观锁适用于读多写少的情况下(多读场景),悲观锁比较适用于写多读少场景


2. 乐观锁的实现方式-CAS(Compare and Swap),CAS(Compare and Swap)实现原理

背景

在jdk1.5之前都是使用synchronized关键字保证同步,synchronized保证了无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量,导致会存在这些问题:

  • 在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
  • 如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险

为了优化悲观锁这些问题,就出现了乐观锁:

假设没有并发冲突,每次不加锁操作同一变量,如果有并发冲突导致失败,则重试直至成功。

CAS(Compare and Swap)原理

CAS 全称是 compare and swap(比较并且交换),是一种用于在多线程环境下实现同步功能的机制,其也是无锁优化,或者叫自旋,还有自适应自旋。

在jdk中,CAS加volatile关键字作为实现并发包的基石。没有CAS就不会有并发包,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。

乐观锁的一种典型实现机制(CAS):

乐观锁主要就是两个步骤:

  • 冲突检测
  • 数据更新

当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。

在不使用锁的情况下保证线程安全,CAS实现机制中有重要的三个操作数:

  • 需要读写的内存位置(V)
  • 预期原值(A)
  • 新值(B)

首先先读取需要读写的内存位置(V),然后比较需要读写的内存位置(V)和预期原值(A),如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。具体可以分成三个步骤:

  • 读取(需要读写的内存位置(V))
  • 比较(需要读写的内存位置(V)和预期原值(A))
  • 写回(新值(B))

3. CAS在JDK并发包中的使用

在JDK1.5以上 java.util.concurrent(JUC java并发工具包)是基于CAS算法实现的,相比于synchronized独占锁,堵塞算法,CAS是非堵塞算法的一种常见实现,使用乐观锁JUC在性能上有了很大的提升。

CAS如何在不使用锁的情况下保证线程安全,看并发包中的原子操作类AtomicInteger::getAndIncrement()方法(相当于i++的操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// AtomicInteger中
//value的偏移量
private static final long valueOffset;
//获取值
private volatile int value;
//设置value的偏移量
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//增加1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • 首先value必须使用了volatile修饰,这就保证了他的可见性与有序性
  • 需要初始化value的偏移量
  • unsafe.getAndAddInt通过偏移量进行CAS操作,每次从内存中读取数据然后将数据进行+1操作,然后对原数据,+1后的结果进行CAS操作,成功的话返回结果,否则重试直到成功为止。
1
2
3
4
5
6
7
8
9
10
java复制代码//unsafe中
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//使用偏移量获取内存中value值
var5 = this.getIntVolatile(var1, var2);
//比较并value加+1
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}

JAVA实现CAS的原理,unsafe::compareAndSwapInt是借助C来调用CPU底层指令实现的。下面是sun.misc.Unsafe::compareAndSwapInt()方法的源代码:

1
2
java复制代码public final native boolean compareAndSwapInt(Object o, long offset,
int expected, int x);

4. CAS的缺陷

ABA问题

在多线程场景下CAS会出现ABA问题,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下

线程1,期望值为A,欲更新的值为B
线程2,期望值为A,欲更新的值为B

线程3,期望值为B,欲更新的值为A

  • 线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,
  • 这个时候出现了线程3,线程3取值与期望的值B比较,发现相等则将值更新为A
  • 此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。

ABA问题带来的危害:

小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50

线程1(提款机):获取当前值100,期望更新为50,

线程2(提款机):获取当前值100,期望更新为50,

线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50

线程3(默认):获取当前值50,期望更新为100,

这时候线程3成功执行,余额变为100,

线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!

此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。

解决方法

  • AtomicStampedReference 带有时间戳的对象引用来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
1
2
3
4
5
6
7
java复制代码public boolean compareAndSet(
V expectedReference,//预期引用
V newReference,//更新后的引用
int expectedStamp, //预期标志
int newStamp //更新后的标志

)
  • 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A

循环时间长开销大

自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来极大的执行开销。

解决方法:

  • 限制自旋次数,防止进入死循环
  • JVM能支持处理器提供的pause指令那么效率会有一定的提升,

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性

解决方法:

  • 如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,
  • 可以把多个共享变量合并成一个共享变量进行CAS操作。

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
欢迎扫码关注,原创技术文章第一时间推出

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!

欢迎扫码关注,原创技术文章第一时间推出

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Java 路线」 为什么 Java 实现了平台无关性?

发表于 2020-08-30

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 从最初学习Java开始,我们就知道Java的口号是:“一次编写,到处运行”。没有了平台的束缚,使得我们再编写Java时并不需要(那么)关心将来运行程序的平台。
  • 那么,Java是如何实现 平台无关性的呢?今天我们来讨论这个问题。

# 咬文嚼字 #

为什么是“不需要(那么)关心”,而不是“不需要关心”?因为在工程实践中,不全面考量运行程序的系统 / 网络 / 硬件 / 国家等因素是不可能的。

目录

  1. 运行环境 = 操作系统 + 硬件

首先,理解清楚什么是平台?平台是指程序的运行平台,或者称为运行环境,具体来说:运行环境 = 操作系统 + 硬件(主要是CPU):

1.1 操作系统屏蔽了除 CPU 外的硬件差异

操作系统(Operating System)是管理计算机硬件与软件资源的程序。对于现代应用程序来说,它们是不会直接操作硬件的,而是采用向操作系统发送指令的方式来间接控制硬件,这些指令就是系统调用。

系统调用是操作系统与应用程序之间的接口(Application Programming Interface,API)。然而不同操作系统提供的 API 是不同的,这样的话,程序调用 API 的代码也会因操作系统不同而不同,因此操作系统是运行环境的要素之一。

应用通过系统调用间接控制硬件

1.2 CPU 只能运行本地代码

每种CPU只能“读懂”自身支持的机器语言或者本地代码(native code),而每种CPU使用的指令集不尽相同。因此,任何高级编程语言 / 汇编语言编写的程序,最后都需要“翻译”为CPU能够读懂的本地代码。

本地代码是 CPU 唯一的语言

下面,我们对比C/C++ & Java两种语言是使用什么方式将源代码转换为本地代码的。


  1. C/C++ 如何将源代码转换为本地代码

  • 步骤1:编译生成目标文件(编译时)
  • 步骤2:链接生成可执行文件(编译时 or 运行时)
  1. Java 如何将源代码转换为本地代码

  • 步骤1:编译前端生成 Class文件(编译时)
  • 步骤2:编译后端解释或编译为本地代码(编译时 or 运行时)

关于 Java 编译过程 的更多介绍,请阅读文章:《Java | 聊一聊编译过程(编译前端 & 编译后端)》


  1. Java 虚拟机的公有协议与私有实现

  • Java 虚拟机的协议 指的是《Java 虚拟机规范》,它规定了 Java 虚拟机的概念模型;
  • Java 虚拟机实现 是指各种平台上具体的虚拟机实现,例如 Classic VM、HotSpot VM;

在这个概念模型下,不同的虚拟机实现有统一的输入输出模型:

所有虚拟机实现的输入与输出都是一致的:输入为Class 文件,处理过程是字节码解释执行的等效过程,最终输出的是预期的执行结果。这样的方式即保证了不同平台不同实现的虚拟机行为一致,也提高了伸缩性。不同平台的虚拟机实现可以根据具体平台特点,赋予虚拟机实现更多的特点:更高的性能 or 更低的内存消耗 。

关于 Class 文件 的更多介绍,请阅读文章:《Java | 请概述一下 Class 文件的结构》


  1. 总结

虚拟机与字节码是Java实现无关性的基础。首先,与不同于C/C++,Java将程序存储格式从本地代码转变为字节码;其次,不同平台的虚拟机都统一采样字节码作为输入语言,并统一遵守《Java 虚拟机规范》,最终提供了一个不依赖于特定操作系统 & 硬件的运行环境,即平台无关性。


参考资料

  • 《程序是怎样跑起来的》 (第7、8章)—— 矢泽久雄
  • 《深入理解Java虚拟机(第3版本)》(第6、7、8章)—— 周志明
  • 《深入理解Android:Java虚拟机ART》(第2章) —— 邓凡平

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一个 Android MVVM 组件化架构框架 Androi

发表于 2020-08-30

AndroidBaseFrameMVVM 🐽

AndroidBaseFrameMVVM 是一个Android工程框架,所使用技术栈为:组件化、Kotlin、MVVM、Jetpack、Repository、Kotlin-Coroutine-Flow,本框架既是一个可以开箱即用的工程框架基础层,也是一个很好的学习资源,文档下面会对框架中所使用的一些核心技术进行阐述。该框架作为个人技术积累的产物,会一直更新维护,如果有技术方面的谈论或者框架中的错误点,可以在 GitHub 上提 Issues,我会及时进行回应。希望这个框架项目能给大家带来帮助,喜欢可以Start🌟。

  项目地址:AndroidBaseFrameMVVM

Demo

  demo是在另一个demo分支,但是目前的demo分支的代码是以前那一版的demo,目前版本的demo还没有来得及编写,敬请期待吧。

框架图示

谷歌 Android 团队 Jetpack 视图模型:

Jetpack 视图模型

模块

  • app:

app壳 工程,是依赖所有组件的壳,该模块不应该包含任何代码,它只作为一个空壳存在,由于项目中使用了EventBusAPT技术,需要索引到各业务组件的对应的APT生成类,所以在 app壳 内有这一部分的代码。

  • buildGradleScript:

脚本模块,该模块下存放的都是各个组件及封装的一些 Gradle 脚本文件。初衷是将所有的脚本统一管理,事实上我在组件内查找脚本的习惯还是没有改掉。

  • buildSrc:

这是一个特殊的文件夹,负责项目的构建,里面存放着一些项目构建时用到的东西,比如项目配置,依赖。这里面还是存放 Gradle 插件的地方,一些自定义的 Gradle 的插件都需要放在此处。

  • lib_base:

项目的基础公共模块,存放着各种基类封装、对远程库的依赖、以及工具类、三方库封装,该组件是和项目业务无关的,和项目业务相关的公共部分需要放在 lib_common 中。

  • lib_common:

项目的业务公共模块,这里面存放着项目里各个业务组件的公共部分,还有一些项目特定需要的一些文件等,该组件是和项目业务有关系的。

  • lib_net:

网络模块,网络模块的配置、封装等,专门设立了一个组件来负责网路模块部分。

组件化相关

组件初始化

为了更好的代码隔离与解耦,在特定组件内使用的SDK及三方库,应该只在该组件内依赖,不应该让该组件的特定SDK及三方库的API暴露给其他不需要用的组件。有一个问题就出现了,SDK及三方库常常需要手动去初始化,而且一般都需要在项目一启动(即 Application 中)初始化,但是一个项目肯定只能有一个自定义的 Application,该项目中的自定义 Application 在 lib_base 模块中,并且也是在 lib_base 模块中的清单文件中声明的,那其他组件该如何初始化呢?带着这个问题我们一起来深入研究下。

常见的组件初始化解决方案:

在我的了解范围内,目前有两种最为常见的解决方案:

  • 面向接口编程 + 反射扫描实现类:

  该方案是基于接口编程,自定义 Application 去实现一个自定义的接口(interface),这个接口中定一些和 Application 生命周期相对应的抽象方法及其他自定义的抽象方法,每个组件去编写一个实现类,该实现类就类似于一个假的自定义 Application,然后在真正的自定义 Application 中去通过反射去动态查找当前运行时环境中所有该接口的实现类,并且去进行实例化,然后将这些实现类收集到一个集合中,在 Application 的对应声明周期方法中去逐一调用对应方法,以实现各实现类能够和 Application 生命周期相同步,并且持有 Application 的引用及 context 上下文对象,这样我们就可以在组件内模拟 Application 的生命周期并初始化SDK和三方库。使用反射还需要做一些异常的处理。该方案是我见过的最常见的方案,在一些商业项目中也见到过。

  • 面向接口编程 + meta-data + 反射:

  该方案的后半部分也是和第一种方法一样,通过接口编程实现 Application 的生命周期同步,其实这一步是避免不了的,在我的方案中,后半部分也是这样实现的。不同的是前半部分,也就是如何找到接口的实现类,该方案使用的是 AndroidManifest 的 meta-data 标签,通过每个组件内的 AndroidManifest 内去声明一个 meta-data 标签,包含该组件实现类的信息,然后在 Application 中去找到这些配置信息,然后通过反射去创建这些实现类的实例,再将它们收集到一个集合中,剩下的操作基本相同了。该方案和第一种方案一样都需要处理很多的异常。这种方案我在一些开源项目中见到过,个人认为过于繁琐,还要处理很多的异常。

本项目中所使用的方案:

  • 面向接口编程 + Java的SPI机制(ServiceLoader)+AutoService:

  先来认识下 Java 的 SPI 机制:面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。JavaSPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移到了程序之外。这段话也是我复制的别人的,听起来很懵逼,大致意思就是我们可以通过 SPI 机制将实现类暴露出去。关于如何使用 SPI,这里不在陈述,总之是我们在各组件内通过 SPI 去将实现类暴露出去,在 Application 中我们通过 Java 提供的 SPI API 去获取这些暴露的服务,这样我们就拿到了这些类的实例,剩下的步骤就和上面的方案一样了,通过一个集合遍历实现类调用其相应的方法完成初始化的工作。由于使用 SPI 需要在每个模块创建对应的文件配置,这比较麻烦,所以我们使用 Google 的 AutoService 库来帮助我们自动创建这些配置文件,使用方式也非常的简单,就是在实现类添加一个 AutoService 注解。本框架中的核心类是这几个:lib_base-LoadModuleProxy、lib_base-ApplicationLifecycle。这种方案是我请教的一个米哈游的大佬,这位大佬告诉我在组件化中组件的初始化可以使用 ServiceLoader 来做,于是我就去研究了下,最后发现这种方案还不错,比前面提到的两种方案都要简单、安全。

资源命名冲突

  在组件化方案中,资源命名冲突是一个比较严重的问题,由于在打包时会进行资源的合并,如果两个模块中有两个相同名字的文件,那么最后只会保留一份,如果不知道这个问题的小伙伴,在遇到这个问题时肯定是一脸懵逼的状态。问题既然已经出现,那我们就要去解决,解决办法就是每个组件都用固定的命名前缀,这样就不会出现两个相同的文件的现象了,我们可以在 build.gradle 配置文件中去配置前缀限定,如果不按该前缀进行命名,AS 就会进行警告提示,配置如下:

1
2
3
Groovy复制代码android {
resourcePrefix "前缀_"
}

组件划分

  其实组件的划分一直是一个比较难的部分,这里其实也给不到一些非常适合的建议,看是看具体项目而定。

  关于基础组件通常要以独立可直接复用的角度出现,比如网络模块、二维码识别模块等。

  关于业务组件,业务组件一般可以进行单独调试,也就是可以作为 app 运行,这样才能发挥组件化的一大用处,当项目越来越大,业务组件越来越多时,编译耗时将会是一个非常棘手的问题,但是如果每个业务模块都可以进行的单独调试,那就大大减少了编译时间,同时,开发人员也不需要关注其他组件。

  关于公共模块,lib_base 放一些基础性代码,属于框架基础层,不应该和项目业务有牵扯,而和项目业务相关的公共部分则应该放在 lib_common 中,不要污染 lib_base。

依赖版本控制

  组件化常见的一个问题就是依赖版本,每个组件都有可能自己的依赖库,那我们应该统一管理各种依赖库及其版本,使项目所有使用的依赖都是同一个版本,而不是不同版本。本项目中使用 buildSrc 中的几个kt文件进行依赖版本统一性的管理,及其项目的一些配置。

MVVM相关

  • MVVM 采用 Jetpack 组件 + Repository 设计模式 实现,所使用的 Jetpack 并不是很多,像 DataBinding、Hilt、Room 等并没有使用,如果需要可以添加。采用架构模式目的就是为了解偶代码,对代码进行分层,各模块各司其职,所以既然使用了架构模式那就要遵守好规范。
  • Repository 仓库层负责数据的提供,ViewModel 无需关心数据的来源,Repository 内避免使用 LiveData,框架里使用了 Kotlin 协程的 Flow 进行处理请求或访问数据库,Repository 的函数会返回一个 Flow 给 ViewModel 的调用函数,Flow 上游负责提供数据,下游也就是 ViewModel 获取到数据使用 LiveData 进行存储,View 层订阅 LiveData,实现数据驱动视图
  • 三者的依赖都是单向依赖,View -> ViewModel -> Repository

项目使用的三方库及其简单示例和资料

  • Kotlin
  • Kotlin-Coroutines-Flow
  • Lifecycle
  • ViewModel
  • LiveData
  • ViewBinding
  • Hilt
  • Android KTX
  • OkHttp:网络请求
  • Retrofit:网络请求
  • MMKV:腾讯基于 mmap 内存映射的 key-value 本地存储组件
  • Glide:快速高效的 Android 图片加载库
  • ARoute:阿里用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
  • BaseRecyclerViewAdapterHelper:一个强大并且灵活的 RecyclerViewAdapter
  • StatusBarUtil:状态栏
  • EventBus:适用于 Android 和 Java 的发布/订阅事件总线
  • Bugly:腾讯异常上报及热更新(只集成了异常上报)
  • PermissionX:郭霖权限请求框架
  • LeakCanary:Android 的内存泄漏检测库
  • AndroidAutoSize:JessYan 大佬的 今日头条屏幕适配方案终极版

Kotlin协程

关于 Kotlin 协程,是真的香,具体教程可以看我的一篇文章:

  • 万字长文 - Kotlin 协程进阶

Flow 类似于 RxJava,它也有一系列的操作符,资料:

  • Google 推荐在 MVVM 架构中使用 Kotlin Flow:
  • 即学即用Kotlin - 协程:
  • Kotlin Coroutines Flow 系列(1-5):

PermissionX

PermissionX 是郭霖的一个权限申请框架
使用方式:

1
2
3
kotlin复制代码PermissionX.init(this)
.permissions("需要申请的权限")
.request { allGranted, grantedList, deniedList -> }

资料:

GitHub: github.com/guolindev/P…

EventBus APT

事件总线这里选择的还是 EventBus,也有很多比较新的事件总线框架,还是选择了这个直接上手的
在框架内我对 EventBus 进行了基类封装,自动注册和解除注册,在需要注册的类上添加 @EventBusRegister 注解即可,无需关心内存泄漏及没及时解除注册的情况,基类里已经做了处理

1
2
kotlin复制代码@EventBusRegister
class MainActivity : AppCompatActivity() {}

很多使用 EventBus 的开发者其实都没有发现 APT 的功能,这是 EventBus3.0 的重大更新,使用 EventBus APT 可以在编译期生成订阅类,这样就可以避免使用低效率的反射,很多人不知道这个更新,用着3.0的版本,实际上却是2.0的效率。
项目中已经在各模块中开启了 EventBus APT,EventBus 会在编译器对各模块生成订阅类,需要我们手动编写代码去注册这些订阅类:

1
2
3
4
5
kotlin复制代码// 在APP壳的AppApplication类中
EventBus
.builder()
.addIndex("各模块生成的订阅类的实例 类名在base_module.gradle脚本中进行了设置 比如 module_home 生成的订阅类就是 module_homeIndex")
.installDefaultEventBus()

屏幕适配 AndroidAutoSize

屏幕适配使用的是 JessYan 大佬的 今日头条屏幕适配方案终极版

GitHub: github.com/JessYanCodi…

使用方式:

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
ini复制代码// 在清单文件中声明
<manifest>
<application>
// 主单位使用dp 没设置副单位
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
</application>
</manifest>

// 默认是以竖屏的宽度为基准进行适配
// 如果是横屏项目要适配Pad(Pad适配尽量使用两套布局 因为手机和Pad屏幕宽比差距很大 无法完美适配)
<manifest>
<application>
// 以高度为基准进行适配 (还需要手动代码设置以高度为基准进行适配) 目前以高度适配比宽度为基准适配 效果要好
<meta-data
android:name="design_height_in_dp"
android:value="400"/>
</application>
</manifest>

// 在Application 中设置
// 屏幕适配 AndroidAutoSize 以横屏高度为基准进行适配
AutoSizeConfig.getInstance().isBaseOnWidth = false

ARoute

ARoute 是阿里巴巴的一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

使用方式:

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
Java复制代码// 1.在需要进行路由跳转的Activity或Fragment上添加 @Route 注解
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}

// 2.发起路由跳转
ARouter.getInstance()
.build("目标路由地址")
.navigation()

// 3.携带参数跳转
ARouter.getInstance()
.build("目标路由地址")
.withLong("key1", 666L)
.withString("key3", "888")
.withObject("key4", new Test("Jack", "Rose"))
.navigation()

// 4.接收参数
@Route(path = RouteUrl.MainActivity2)
class MainActivity : AppCompatActivity() {

// 通过name来映射URL中的不同参数
@Autowired(name = "key")
lateinit var name: String

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
// ARouter 依赖注入 ARouter会自动对字段进行赋值,无需主动获取
ARouter.getInstance().inject(this)
}
}

// 5.获取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();

资料:

官方文档:github.com/alibaba/ARo…

ViewBinding

通过视图绑定功能,可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。
在大多数情况下,视图绑定会替代 findViewById

使用方式:

按模块启用ViewBinding

1
2
3
4
5
6
7
8
9
10
11
12
groovy复制代码// 模块下的build.gradle文件
android {
// 开启ViewBinding
// 高版本AS
buildFeatures {
viewBinding = true
}
// 低版本AS 最低3.6
viewBinding {
enabled = true
}
}

Activity 中 ViewBinding 的使用

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码// 之前设置视图的方法
setContentView(R.layout.activity_main)

// 使用ViewBinding后的方法
val mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)

// ActivityMainBinding类是根据布局自动生成的 如果没有请先build一下项目
// ViewBinding会将控件id转换为小驼峰命名法,所以为了保持一致规范,在xml里声明id时也请使用小驼峰命名法
// 比如你有一个id为mText的控件,可以这样使用
mBinding.mText.text = "ViewBinding"

Fragment 中 ViewBinding 的使用

1
2
3
4
5
6
kotlin复制代码// 原来的写法
return inflater.inflate(R.layout.fragment_blank, container, false)

// 使用ViewBinding的写法
mBinding = FragmentBlankBinding.inflate(inflater)
return mBinding.root

资料:

官方文档: developer.android.com/topic/libra…

CSDN: blog.csdn.net/u010976213/…

ViewModel

ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

使用方式:

1
2
3
4
5
6
kotlin复制代码class MainViewModel : ViewModel(){}

class MainActivity : AppCompatActivity() {
// 获取无参构造的ViewModel实例
val mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
}

资料:

官方文档: developer.android.com/topic/libra…

Android ViewModel,再学不会你砍我: juejin.cn/post/684490…

LiveData

LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者

LiveData 分为可变值的 MutableLiveData 和不可变值的 LiveData

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码fun test() {
val liveData = MutableLiveData<String>()
// 设置更新数据源
liveData.value = "LiveData"
// 将任务发布到主线程以设置给定值
liveData.postValue("LiveData")
// 获取值
val value = liveData.value
// 观察数据源更改(第一个参数应是owner:LifecycleOwner 比如实现了LifecycleOwner接口的Activity)
liveData.observe(this, {
// 数据源更改后触发的逻辑
})
}

资料:

官方文档: developer.android.com/topic/libra…

Lifecycle

Lifecycle 是一个类,用于存储有关组件(如 Activity 或 Fragment)的生命周期状态的信息,并允许其他对象观察此状态。LifecycleOwner 是单一方法接口,表示类具有 Lifecycle。它具有一种方法(即 getLifecycle()),该方法必须由类实现。实现 LifecycleObserver 的组件可与实现 LifecycleOwner 的组件无缝协同工作,因为所有者可以提供生命周期,而观察者可以注册以观察生命周期。

资料:

官方文档: developer.android.com/topic/libra…

Hilt

Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手动依赖项注入要求您手动构造每个类及其依赖项,并借助容器重复使用和管理依赖项。

Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 **DI(依赖项注入)**的标准方法。Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。

资料:

目前官方文档还没有更新正式版的,还是 alpha 版本的文档:使用 Hilt 实现依赖项注入

Dagger 的 Hilt 文档目前是最新的:Dagger-Hilt

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker 镜像构建之 Dockerfile

发表于 2020-08-28

  在 Docker 中构建镜像最常用的方式,就是使用 Dockerfile。Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。官方文档:docs.docker.com/engine/refe…

  

Dockerfile 常用指令

  

FROM

  

  语法:FROM <image>:<tag>

  指明构建的新镜像是来自于哪个基础镜像,如果没有选择 tag,那么默认值为 latest。

1
shell复制代码FROM centos:7

如果不以任何镜像为基础,那么写法为:FROM scratch。官方说明:scratch 镜像是一个空镜像,可以用于构建 busybox 等超小镜像,可以说是真正的从零开始构建属于自己的镜像。

  

MAINTAINER(deprecated)

  

  语法:MAINTAINER <name>

  指明镜像维护者及其联系方式(一般是邮箱地址)。官方说明已过时,推荐使用 LABEL。

1
shell复制代码MAINTAINER mrhelloworld <mrhelloworld@126.com>

  

LABEL

  

  语法:LABEL <key>=<value> <key>=<value> <key>=<value> ...

  功能是为镜像指定标签。也可以使用 LABEL 来指定镜像作者。

1
shell复制代码LABEL maintainer="mrhelloworld.com"

  

RUN

  

  语法:RUN <command>

  构建镜像时运行的 Shell 命令,比如构建的新镜像中我们想在 /usr/local 目录下创建一个 java 目录。

1
shell复制代码RUN mkdir -p /usr/local/java

  

ADD

  

  语法:ADD <src>... <dest>

  拷贝文件或目录到镜像中。src 可以是一个本地文件或者是一个本地压缩文件,压缩文件会自动解压。还可以是一个 url,如果把 src 写成一个 url,那么 ADD 就类似于 wget 命令,然后自动下载和解压。

1
shell复制代码ADD jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java

  

COPY

  

  语法:COPY <src>... <dest>

  拷贝文件或目录到镜像中。用法同 ADD,只是不支持自动下载和解压。

1
shell复制代码COPY jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java

  

EXPOSE

  

  语法:EXPOSE <port> [<port>/<protocol>...]

  暴露容器运行时的监听端口给外部,可以指定端口是监听 TCP 还是 UDP,如果未指定协议,则默认为 TCP。

1
shell复制代码EXPOSE 80 443 8080/tcp

如果想使得容器与宿主机的端口有映射关系,必须在容器启动的时候加上 -P 参数。

  

ENV

  

  语法:ENV <key> <value> 添加单个,ENV <key>=<value> ... 添加多个。

  设置容器内环境变量。

1
shell复制代码ENV JAVA_HOME /usr/local/java/jdk-11.0.6/

  

CMD

  

  语法:

  • CMD ["executable","param1","param2"],比如:CMD ["/usr/local/tomcat/bin/catalina.sh", "start"]
  • CMD ["param1","param2"] ,比如:CMD [ "echo", "$JAVA_HOME" ]
  • CMD command param1 param2,比如:CMD echo $JAVA_HOME

  启动容器时执行的 Shell 命令。在 Dockerfile 中只能有一条 CMD 指令。如果设置了多条 CMD,只有最后一条 CMD 会生效。

1
shell复制代码CMD ehco $JAVA_HOME

如果创建容器的时候指定了命令,则 CMD 命令会被替代。假如镜像叫 centos:7,创建容器时命令是:docker run -it --name centos7 centos:7 echo "helloworld" 或者 docker run -it --name centos7 centos:7 /bin/bash,就不会输出 $JAVA_HOME 的环境变量信息了,因为 CMD 命令被 echo "helloworld"、/bin/bash 覆盖了。

  

ENTRYPOINT

  

  语法:

  • ENTRYPOINT ["executable", "param1", "param2"],比如:ENTRYPOINT ["/usr/local/tomcat/bin/catalina.sh", "start"]
  • ENTRYPOINT command param1 param2,比如:ENTRYPOINT ehco $JAVA_HOME

  启动容器时执行的 Shell 命令,同 CMD 类似,不会被 docker run 命令行指定的参数所覆盖。在 Dockerfile 中只能有一条 ENTRYPOINT 指令。如果设置了多条 ENTRYPOINT,只有最后一条 ENTRYPOINT 会生效。

1
shell复制代码ENTRYPOINT ehco $JAVA_HOME
  • 如果在 Dockerfile 中同时写了 ENTRYPOINT 和 CMD,并且 CMD 指令不是一个完整的可执行命令,那么 CMD 指定的内容将会作为 ENTRYPOINT 的参数;
  • 如果在 Dockerfile 中同时写了 ENTRYPOINT 和 CMD,并且 CMD 是一个完整的指令,那么它们两个会互相覆盖,谁在最后谁生效

  

WORKDIR

  

  语法:WORKDIR /path/to/workdir

  为 RUN、CMD、ENTRYPOINT 以及 COPY 和 AND 设置工作目录。

1
shell复制代码WORKDIR /usr/local

  

VOLUME

  

  指定容器挂载点到宿主机自动生成的目录或其他容器。一般的使用场景为需要持久化存储数据时。

1
2
shell复制代码# 容器的 /var/lib/mysql 目录会在运行时自动挂载为匿名卷,匿名卷在宿主机的 /var/lib/docker/volumes 目录下
VOLUME ["/var/lib/mysql"]

一般不会在 Dockerfile 中用到,更常见的还是在 docker run 的时候通过 -v 指定数据卷。

  

构建镜像

  

  Dockerfile 文件编写好以后,真正构建镜像时需要通过 docker build 命令。

  docker build 命令用于使用 Dockerfile 创建镜像。

1
2
3
4
shell复制代码# 使用当前目录的 Dockerfile 创建镜像
docker build -t mycentos:7 .
# 通过 -f Dockerfile 文件的位置创建镜像
docker build -f /usr/local/dockerfile/Dockerfile -t mycentos:7 .
  • -f:指定要使用的 Dockerfile 路径;
  • --tag, -t:镜像的名字及标签,可以在一次构建中为一个镜像设置多个标签。

  

关于 . 理解

  

  我们在使用 docker build 命令去构建镜像时,往往会看到命令最后会有一个 . 号。它究竟是什么意思呢?

  很多人以为是用来指定 Dockerfile 文件所在的位置的,但其实 -f 参数才是用来指定 Dockerfile 的路径的,那么 . 号究竟是用来做什么的呢?

  Docker 在运行时分为 Docker 引擎(服务端守护进程) 和 客户端工具,我们日常使用各种 docker 命令,其实就是在使用 客户端工具 与 Docker 引擎 进行交互。

  当我们使用 docker build 命令来构建镜像时,这个构建过程其实是在 Docker 引擎 中完成的,而不是在本机环境。如果在 Dockerfile 中使用了一些 ADD 等指令来操作文件,如何让 Docker 引擎 获取到这些文件呢?

  这里就有了一个 镜像构建上下文 的概念,当构建的时候,由用户指定构建镜像时的上下文路径,而 docker build 会将这个路径下所有的文件都打包上传给 Docker 引擎,引擎内将这些内容展开后,就能获取到上下文中的文件了。

  

  举个栗子:我的宿主机 jdk 文件在 /root 目录下,Dockerfile 文件在 /usr/local/dockerfile 目录下,文件内容如下:

1
shell复制代码ADD jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java

  那么构建镜像时的命令就该这样写:

1
shell复制代码docker build -f /usr/local/dockerfile/Dockerfile -t mycentos:7 /root

  

  再举个栗子:我的宿主机 jdk 文件和 Dockerfile 文件都在 /usr/local/dockerfile 目录下,文件内容如下:

1
shell复制代码ADD jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java

  那么构建镜像时的命令则这样写:

1
shell复制代码docker build -f /usr/local/dockerfile/Dockerfile -t mycentos:7 .

  

Dockerfile 实践

  

  接下来我们通过基础镜像 centos:7,在该镜像中安装 jdk 和 tomcat 以后将其制作为一个新的镜像 mycentos:7。

  创建目录。

1
shell复制代码mkdir -p /usr/local/dockerfile

  编写 Dockerfile 文件。

1
shell复制代码vi Dockerfile

  Dockerfile 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shell复制代码# 指明构建的新镜像是来自于 centos:7 基础镜像
FROM centos:7
# 通过镜像标签声明了作者信息
LABEL maintainer="mrhelloworld.com"
# 设置工作目录
WORKDIR /usr/local
# 新镜像构建成功以后创建指定目录
RUN mkdir -p /usr/local/java && mkdir -p /usr/local/tomcat
# 拷贝文件到镜像中并解压
ADD jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java
ADD apache-tomcat-9.0.37.tar.gz /usr/local/tomcat
# 暴露容器运行时的 8080 监听端口给外部
EXPOSE 8080
# 设置容器内 JAVA_HOME 环境变量
ENV JAVA_HOME /usr/local/java/jdk-11.0.6/
ENV PATH $PATH:$JAVA_HOME/bin
# 启动容器时启动 tomcat
CMD ["/usr/local/tomcat/apache-tomcat-9.0.37/bin/catalina.sh", "run"]

  构建镜像。

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
shell复制代码[root@localhost ~]# docker build -f /usr/local/dockerfile/Dockerfile -t mycentos:7 /root/
Sending build context to Docker daemon 191.4MB
Step 1/10 : FROM centos:7
---> 7e6257c9f8d8
Step 2/10 : LABEL maintainer="mrhelloworld.com"
---> Running in 3f18aa4f3fb2
Removing intermediate container 3f18aa4f3fb2
---> 7364f68ca4ab
Step 3/10 : WORKDIR /usr/local
---> Running in d9889152cfc4
Removing intermediate container d9889152cfc4
---> d05bd2e09fa4
Step 4/10 : RUN mkdir -p /usr/local/java && mkdir -p /usr/local/tomcat
---> Running in 3bcd6ef78350
Removing intermediate container 3bcd6ef78350
---> 4832abf9d769
Step 5/10 : ADD jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java
---> e61474bf7a76
Step 6/10 : ADD apache-tomcat-9.0.37.tar.gz /usr/local/tomcat
---> 7110cdff7438
Step 7/10 : EXPOSE 8080
---> Running in a4731c1cf77d
Removing intermediate container a4731c1cf77d
---> f893cefee00c
Step 8/10 : ENV JAVA_HOME /usr/local/java/jdk-11.0.6/
---> Running in f0cb08f390db
Removing intermediate container f0cb08f390db
---> ff9f6acf6844
Step 9/10 : ENV PATH $PATH:$JAVA_HOME/bin
---> Running in eae88cf841d0
Removing intermediate container eae88cf841d0
---> 4b9226a23b10
Step 10/10 : CMD ["/usr/local/tomcat/apache-tomcat-9.0.37/bin/catalina.sh", "run"]
---> Running in ccf481045906
Removing intermediate container ccf481045906
---> 9ef76a16441b
Successfully built 9ef76a16441b
Successfully tagged mycentos:7

  

镜像构建历史

  

1
2
shell复制代码docker history 镜像名称:标签|ID
docker history mycentos:7

  

使用构建的镜像创建容器

  

1
2
3
4
5
6
7
8
9
10
shell复制代码# 创建容器
docker run -di --name mycentos7 -p 8080:8080 mycentos:7
# 进入容器
docker exec -it mycentos7 /bin/bash
# 测试 java 环境变量
[root@dcae87df010b /]# java -version
java version "11.0.6" 2020-01-14 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.6+8-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.6+8-LTS, mixed mode)
# 访问 http://192.168.10.10:8080/ 看到页面说明环境 OK!

太棒了,Dockerfile 构建镜像的方式你也学会了,再接再厉学习一下 Docker 镜像的备份恢复迁移,go ~

本文采用 知识共享「署名-非商业性使用-禁止演绎 4.0 国际」许可协议。

大家可以通过 分类 查看更多关于 Docker 的文章。

  

🤗 您的点赞和转发是对我最大的支持。

📢 扫码关注 哈喽沃德先生「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…784785786…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%