记一次代码重构

最近在编写Service层的时候,出现了这样的问题:

由于对数据合法性的要求比较高,光靠外键显然不能胜任,因此我全面抛弃外键,在Service层用大量代码校验数据的合法性。

于是就出现了这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public Map editXXX(Entity entity){

Map result = new Map<>;
boolean flag = true;
if(entity.getparam1() is invaild){
flag = false;
logger.log;
result.put("msg","param1 is invaild");
}
...(param2)

result.put("result",flag);
if(!flag){
return result;
}

...mapper.Update(entity);
logger.log;
result.put("msg","success");

return result;
}

这里具体检测是否有效的代码比较复杂,例如update方法的id肯定不能是null,积分明细的uid必须在user表里查得到。

这样写着写着,写了五个Service之后开始觉得迷茫,懵逼,姜硬,可读性、可维护性差,耦合度高。

于是萌生出重构的想法,某人一语惊醒我把这些if独立成一个方法

直接独立肯定不行,我Service是面向接口写的,难道在接口中先定义好这样的check方法?

本来想将它抽出来做切面,但是毕竟这是业务逻辑的核心代码,不是日志和性能这些边缘功能,AOP印象里是改变不了方法某个局部变量的值的……

看了一眼之前整合shiro的util包,决定在util包下建一个subpackage叫checker,然后定义baseChecker类,里面定义几个常用方法。

其他具体的实体检查器就继承它,然后将这些checker对象注入Service类进行调用即可。

在定义baseChecker类的时候,由于对Java基础语法不够熟悉,我写出了这样的代码:

1
2
3
4
5
6
public class baseChecker {

public boolean checkNull(Object object,boolean flag){
return flag;
};
}

然后我发现这里需要使用的是泛型,而不是Object,而且泛型声明应该放在类里,正确的写法应该是:

1
2
3
4
5
6
public class baseChecker<T> {

public boolean checkNull(T t,boolean flag){
return flag;
};
}

然后子类这么写:

1
2
3
4
5
6
public class userChecker extends baseChecker {
@Override
public boolean checkNull(Object o , boolean flag) {
return flag;
}
}

关于shiro在SSM环境下的整合(2)

接上文

(题外话:好像hexo对Markdown里Java的渲染不是很好,注解会嵌到其他行去?)

编写Controller

做用户认证,首先得有一个Controller,生成一个User对象。由于研究对象是shiro,先不写实际页面,用一个简单的表单向Controller提交对象。

直接上Controller部分代码:

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
@Controller
//省略具体URL路径代码
//此处直接传入表单,或者JSON也可以,总之要绑定User对象。
public String login(User user, BindingResult result, Model model, HttpServletRequest request) {

try {
//获取一个Subject,Subject在Shiro框架中是主体,一般场景中就是登陆的用户。
Subject subject = SecurityUtils.getSubject();
//如果是已经登录的(Session中有相应会话),跳转到上一个访问的页面
if (subject.isAuthenticated()) {
return "redirect:/";
}
//如果传入参数不正确,例如密码是空的
if (result.hasErrors()) {
model.addAttribute("error", "参数错误!");
return "login";
}

// 将表单传入的email和密码存为Shiro的Token
UsernamePasswordToken token = new UsernamePasswordToken(user.getEmail(), user.getPwd());
//并且提交Realm验证
subject.login(token);
//如果认证通过,没有抛出异常
final User authUserInfo = um.getUserByEmail(user.getEmail());
request.getSession().setAttribute("userInfo", authUserInfo);
} catch (AuthenticationException e) {
// 身份验证失败
model.addAttribute("error", "用户名或密码错误 !");
return "login";
}
}

先实例化一个subject,校验后调用.login()方法,并传入包含用户名(可以是能用来登陆的其他东西)和密码。
之前在Spring配置中将Realm注入到SecurityManager,因此会跳转到Realm处理。

(出于业务需求的原因,本文暂时不涉及多个Realm的情况,可以参考http://blog.csdn.net/xiangwanpeng/article/details/54802509

编写Realm具体方法之认证

跳转之后,首先进入doGetAuthenticationInfo方法,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String email = String.valueOf(token.getPrincipal());
String password = new String((char[]) token.getCredentials());
//获取参数,并使用参数在数据库中验证,此处um即为Spring注入的UserMapper
final User authc = um.getIdPwdByEmail(email);
//接下来的逻辑可以自由发挥,总之登录失败就要抛出异常,并且由Controller捕捉
if (password.equals(authc.getPwd())){
//此处getname是获取该类的名字
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(email, password, getName());
return authenticationInfo;
}else{
throw new AuthenticationException("用户名或密码错误.");
}
}

这里使用了userMapper在数据库中取验证信息,当然也可以使用userService封装好的方法,随意发挥。

如果登陆失败,可以自己抛出并处理各种异常,shiro自带用户锁定、同IP尝试次数过多等非常丰富的异常。由于是小系统,不做复杂处理。

假如这个方法通过了,我们会return一个包含了用户名(确切讲是用户标识,根据业务场景不同而不同),密码(其他授权信息也可以)以及当前类名的authcInfo对象。

这个对象这里我们没有使用,但是可以用在JSP标签中,参考此文:http://www.sojson.com/blog/144.html

编写Realm具体方法之授权

如果上一个方法没有抛出异常,会进入doGetAuthorizationInfo方法,传入对象则是一个principalCollection。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String email = String.valueOf(principals.getPrimaryPrincipal());
final User user = um.getUserByEmail(email);
Set set = new HashSet<String>();
if (user.getAdmin() == 1){
set.add("Admin");
}else{
set.add("User");
}
authorizationInfo.setRoles(set);
return authorizationInfo;
}

这个方法的作用,是从数据源中,根据用户标识(邮箱),取出所对应的权限(无论是以字段形式存在于用户实体,或者是另起一张表,都只是形式,目的是要取出权限)。

并且以字符串的set形式,存入authzInfo对象中并且return,return的这个对象稍后可以在JSP中以tag形式调用。也可以直接在Spring配置中写上/admin = authz,使用shiro自带的Filter进行过滤。

至此,使用Shiro在SSM应用中的认证、授权已经完成。这里实现了最简单的功能,系统只有两个角色(用户、管理员),管理员能访问后台系统,用户不能访问,仅此而已。

事实上shiro的功能远远不止这些,多用户、多权限,甚至多数据源、多Realm都可以做到。甚至还提供了密码加盐存储的工具类,而这一切都能和Spring 完美整合。

使用shiro的目的,首先是无需手写Filter拦截每一个没有权限的请求,其次是不必手写代码去判断用户权限,不必手写代码实现每个权限能做的事,也不必手动处理登陆失败的情景,节省大量工作量。


题外话

在之前的学习过程中,我看到的demo为了实现全部功能,将用户、权限全部实现,起了五张表,分别存储用户、权限、角色,用户-角色关系,角色-权限关系。

更丧心病狂的是,权限表还有roleSign,roleName,roleDescription三个字段,而实际上放进authzInfo的只有roleSign一个字符串。

我尝试直接阅读代码,一个Realm三个Service三个实体,让我对功能一头雾水,今早突然醒悟,既然源码都开放了,我何必苦苦猜测功能,直接git clone 打breakpoint debug,模拟一次登录,终于豁然开朗。

关于shiro在SSM环境下的整合(1)

1.无状态场景下的shiro整合。

这是自己练手的产物,github:https://github.com/Arsenolite/com.qhs.blog

分析思路:

  • 由于是无状态API,只有带Token访问,因此把session相关的全部禁用。
  • 而Realm起到的是userService的作用,也禁用掉,毕竟我不可能每次用户请求API就去查数据库吧。
  • 再加上当时使用了JWT作为Token规范,因此业务需求就成了检验JWT有效性。
  • Filter中直接注入redisDAO和tokenService,对URL中的Token参数检验,有效就放行。

具体写法:

首先我们要有一个maven+ssm的环境,并且导入shiro-web,shiro-core和shiro-spring三个依赖。
由于我们需要将shiro纳入Spring的管理周期,在Web.xml中这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- shiro -->
<!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.xml -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

这里因为要和Spring集成,所以具体Filter的代码不是写在这里,这里只是将shiro的Filter纳入Spring的Filter链。

接下来是重头戏:Spring配置文件,既然有SSM环境,肯定有一份或者多份xml。
定义Filter,每一个都指向具体的类(稍后在后文贴出)
1
2
3
4
5
6
7
<!--验证码Filter-->
<bean id="mailFilter" class="com.qhs.blog.util.shiro.filter.mailFilter"/>
<bean id="captchaFilter" class="com.qhs.blog.util.shiro.filter.captchaFilter"/>
<!--认证Filter-->
<bean id="loginFilter" class="com.qhs.blog.util.shiro.filter.loginFilter"/>
<!--授权Filter-->
<bean id="adminFilter" class="com.qhs.blog.util.shiro.filter.adminFilter"/>
组装Filter工厂,之前web.xml中的shiroFilter实际上是一个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
<!-- shiroFilter工厂 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 构建securityManager环境 -->
<property name="securityManager" ref="securityManager"/>
<property name="filters">
<util:map>
<entry key="mail" value-ref="mailFilter"/>
<entry key="captcha" value-ref="captchaFilter"/>
<entry key="admin" value-ref="adminFilter"/>
<entry key="login" value-ref="loginFilter"/>
</util:map>
</property>
<!-- 设计哪些URL用哪个过滤器 -->
<!--由于具体用户权限还没细化成接口,先定义两个验证码的过滤器-->
<property name="filterChainDefinitions">
<value>
<!-- 邮箱验证码要求网页验证码认证通过 -->
/api/captcha/** = noSessionCreation,anon
/api/mail/** = noSessionCreation,captcha
<!-- 用户模块 -->

</value>
</property>
</bean>
接下来将shiro的会话相关功能全部禁用掉,StatelessDefaultSubjectFactory需要手动撰写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Subject工厂,写一个禁用会话的subjectFactory -->
<bean id="subjectFactory" class="com.qhs.blog.util.shiro.StatelessDefaultSubjectFactory"/>

<!-- 禁用掉会话调度器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false"/>
</bean>

<!--解决报错,组装默认的subjectDAO-->
<bean id="subjectDAO" class="org.apache.shiro.mgt.DefaultSubjectDAO">
<property name="sessionStorageEvaluator" ref="sessionStorageEvaluator"/>
</bean>

<bean id="sessionStorageEvaluator" class="org.apache.shiro.mgt.DefaultSessionStorageEvaluator">
<property name="sessionStorageEnabled" value="false"/>
</bean>
将配置为禁用Session的subjectDAO、subjectFactory和sessionManager注入安全管理器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--<property name="realm" ref="userRealm"/>-->
<property name="subjectDAO" ref="subjectDAO"/>
<property name="subjectFactory" ref="subjectFactory"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>

<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>

最后构建一个迭代7次的SHA-512 Hash服务,用来在类里调用。
1
2
3
4
5
<!--构建一个Hash服务,在此指定叠加方式和采用的算法-->
<bean id="defaultHashService" class="org.apache.shiro.crypto.hash.DefaultHashService">
<property name="hashAlgorithmName" value="SHA-512"/>
<property name="hashIterations" value="7"/>
</bean>
前文提到的StatelessDefaultSubjectFactory:
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.qhs.blog.util.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
接下来就是编写具体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
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.qhs.blog.util.shiro.filter;


import com.qhs.blog.serviceImpl.tokenServiceImpl;
import net.minidev.json.JSONObject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
* Created by QHS on 2017/5/31.
*/
public class captchaFilter extends AccessControlFilter {
@Autowired
private tokenServiceImpl tokenService;

JSONObject resp = new JSONObject();

@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
boolean flag = false;
HttpServletRequest req = WebUtils.toHttp(servletRequest);
String token = req.getParameter("token");
Map<String, Object> resultMap = tokenService.validToken(token);
switch ((String) resultMap.get("state")) {
case "EXPIRED":
resp.put("warning", "授权已过期");
break;
case "VALID":
JSONObject jo = (JSONObject) resultMap.get("data");
String value = (String) jo.get("value");
if (value.equals("captchaAuthed") && value.equals("getMailCode")) {
flag = true;
}
break;
case "INVALID":
resp.put("msg", "请输入验证码");
break;
}

return flag;
}

@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("application/json; charset=utf-8");
servletResponse.getWriter().write(resp.toJSONString());
return false;
}

}

这个Filter继承了AccessControllFilter,shiro有几个内置Filter,有的用于认证,有的用于鉴权。而在这里,我只需要一个验证码,没必要处理用户登录,因此直接用了AccessControllFilter。

继承了类就需要继承方法,AccessControllFilter带了isAccessAllowed和onAccessdenied方法。

前者如果return了True,就允许用户访问Filter保护下的url,如果return False,那就会进入后面的方法,如果后面的方法Return True也允许用户访问(意义不明啊)。

因此我在类里定义变量JSONObject,在isAccessAllowed方法中对传入Token进行验证,验证不通过则在JSONObject内写入相应的错误信息并返回false。

进入onAccessDenied方法,在方法体内将JSONObject写入Servlet流输出到网页上,这样就完成了一个基本的无状态应用集成。
(其实我的做法和手写Filter差不多,只是引入shiro作为练手,和后续功能扩展。)

2.普通Web应用中使用shiro

分析思路:

现在在单位做的项目,计划中是一个由服务端渲染JSP的项目,因此可以使用Session和Cookie。

这时候引入shiro,就可以使用realm的相关特性,将userService注入realm中,由shiro接管用户认证、授权的成功/失败后的场景。可以说是充分发挥了功能。
Web.xml内容不变,Spring配置中去掉那些关于Session的设置,并且也不用写StatelessDefaultSubjectFactory。

具体写法:

修改XML配置
1
2
3
4
5
6
7
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
</bean>

<bean id="userRealm" class="com.shengting.store.util.shiro.realm.userRealm">
<property name="cachingEnabled" value="false"/>
</bean>
新建userRealm

由于这个Realm类需要同时完成认证/鉴权工作(认证(Authc)指的是判断用户是否能登录,而鉴权(Authz)指的是用户有没有权限执行某项操作。),我们让它继承AuthorizingRealm类。

继承了类就要继承方法,我们的类看起来是这样子的:

1
2
3
4
5
6
7
8
9
10
public class userRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}

后面的大体思路,就是调用userMapper在数据库中查找信息(也可以使用封装好的userService)
由于工作进度问题,Controller还没有写完,暂时先更新到这里;w;

#125;

这个use()方法本质是调用close()方法,见这个回答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//java
XXXBo bo = null;
bo.getXXX();//NPE

//kotlin
//编译错误:Null can not be a value of a non-null type XXXBo
//var bo:XXXBo = null;

var bo:XXXBo? = null;

//编译错误:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type XXXBo
//bo.count;

if(bo !=null){
bo.count;
}

Java里其实有个叫Lombok的东西干了差不多的事,在编译器自动生成Getter Setter equals() toString()这么些方法。
不过Kotlin里调用get/set方法也可以不用方法名,而是直接.属性名。
顺带一提,Kotlin的主方法由于没有static方法,用的是伴生对象,并且方法加入@JvmStatic注解。
不过IDEA似乎没法用psvm来快速生成了,也不知道是复杂了还是简单了(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//java
public class XXXBo{
private Integer id;
//getter setter
}
psvm{
new XXXBo().getId();
}
//kotlin
data class XXXBo(val id:Int){

}
companion object {
@JvmStatic
fun main(args: Array<String>) {
XXXBo().id;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//java

//TODO xxx
//完全依赖IDE语法高亮,对实际编译的代码来说就是空代码

//kotlin
//标准库提供TODO函数,执行此处代码会抛出一个异常

TODO()

//@kotlin.internal.InlineOnly
//public inline fun TODO(): Nothing = throw NotImplementedError()

详见链接

jOOQ比起MyBatis的便利之处

单位新项目用了新的持久化框架,叫jOOQ

用惯MyBatis的我一开始有些不适应,但是写过新项目之后,回去维护老项目,确实感觉基于XML的MyBatis编码效率低下。

首先,两边同样有自动生成代码的工具。
MyBatis生成的有Dao层和pojo,需要配置生成哪张表的pojo,(随口一提,公司的生成器配置可能有bug,生成XML时并不会清空之前的XML内容,需要手动清空/删除一次。)

生成的dao提供的方法有:

  • 根据主键获取
  • 插入
  • 可选插入(pojo对应字段不是null的时候才设值)
  • 更新
  • 可选更新(不是null的时候才更新值)
  • 逻辑删除

pojo则是数据库的各字段以及get/set方法,值得一提的是每个字段上会有DDL里的列备注,但是会充斥大量空行,阅读起来较为困难。

而jOOQ的代码生成器一次性生成所有表的dao层和pojo。dao层全部继承了一个叫DAOImpl的抽象类,提供了以下方法:

  • 新增单个pojo
  • 新增多个pojo(可变参数)
  • 新增多个pojo(集合)
  • 用以上三种方式更新、物理删除pojo
  • 根据主键判断是否存在
  • 获取表中记录总数
  • 获取整张表
  • 利用Java8的Optional来获取(大概是允许参数为null?)
  • 获取主键

还有一些看不太懂的方法:

  • private /* non-final */ Condition equal(Field<?>[] pk, Collection<T> ids)
  • private /* non-final */ List<R> records(Collection<P> objects, boolean forUpdate)
  • private /* non-final */ RecordListenerProvider[] providers(final RecordListenerProvider[] providers, final Object object)

而每个表不同的dao实现类也有各自的方法:

  • 根据主键(项目里就是id)获取
  • 根据多个id获取多个记录(可变参数)
  • 根据每个唯一索引获取记录
  • 根据每个列获取多条记录,也支持可变参数

比MyBatis丰富很多,唯一的缺陷是没有自动生成的逻辑删除方法,初次维护项目很容易根据直觉使用delete方法,需要自动生成Service层代码进行封装。

生成的pojo则兼容了JPA的注解(目前没有在项目里用上),这方面有点hibernate的画风? 较MyBatis生成的pojo要紧凑很多,少了很多无谓的空行。
当业务变化,建立新表,jOOQ生成的代码可以快速满足大部分业务。

说完自动生成的代码,接下来谈谈编写业务代码的复杂度。
贴一段使用MyBatis的传统项目里的单表分页查询代码:

首先需要编写XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="对应的dao接口类名">
<select id="selectByPageQuery"
resultMap="自动生成的Mapper里的ResultMap">
select * from 表
where is_deleted = 0
order by id desc
LIMIT #{itemIndex}, #{pageSize}
</select>

<select id="countByPageQuery" resultType="java.lang.Integer">
select count(*) from 表
where is_deleted = 0
LIMIT #{itemIndex}, #{pageSize}
</select>
</mapper>

由于多个Mapper需要公用LIMIT #{itemIndex}, #{pageSize} 这段语句(还有一些权限控制的语句),事实上代码是这样的:

1
<include refid="com.***.dao.mapper.fragment.PageMapper.pageLimit"/>

所以无法使用注解方式的SQL。
接下来是接口:

1
2
3
4
public interface 表SimpleSelectDao {
int countByPageQuery(PageQuery query);
List<自动生成的实体类> selectByPageQuery(PageQuery query);
}

其中PageQuery是公司封装的分页类,不贴代码了,可以从SQL里看出用到的字段。
然后再在Service层使用:

1
2
3
4
5
...
int count = parcelTransferRecordSimpleSelectDao.countByPageQuery(query);
query.setItemTotal(count);
List<TxParcelTransferRecord> records = parcelTransferRecordSimpleSelectDao.selectByPageQuery(query);
...

相对于基于XML的MyBatis,jOOQ在业务变动时编写代码的效率更高,同样是分页获取代码,只需要7行:

说明:
Pageable是Spring 5的分页相关类,专门解决从前端传参到数据库查询的分页问题,不需要自己实现分页框架了。
Tuple2是公司内部实现的数据结构,用来返回两个不同类型的数据。
SelectConditionStepdsl是jOOQ提供的类和对象,其中dsl的类是DSLContext,用于执行SQL,改写生成器后可以使用Spring注入。

1
2
3
4
5
6
7
8
9
public Tuple2<List<表实体类>, Integer> pageVoById(Pageable pageable) {
SelectConditionStep<表Record类> step = dsl.selectFrom(表的枚举)
.where(表的枚举.IS_DELETED.eq(false));
List<表实体类> list = step.orderBy(表的枚举.ID.desc())
.limit((int) pageable.getOffset(), pageable.getPageSize())
.fetchInto(表实体类.class);
int count = dsl.fetchCount(step);
return new Tuple2<>(list, count);
}

这段代码出现在Service层,用惯之后很符合人类直觉,发挥了SQL人类可读性好的优势。

jOOQ封装了sql,将所有的字段和SQL语句化为Java代码,当然也可以传入完整或者部分的sql执行,自由度很高,同时有Java强类型的优势加持,屏蔽掉了MyBatis的ResultMapper环节,每一步SQL执行虽然要使用一些复杂的类和对象(例如SelectConditionStep),对习惯使用强类型语言的java码狗来说很有“安全感”。

相比jOOQ,基于XML的MyBatis代码编写繁琐,手写SQL经常漏掉order by id desc(页面上刚刚新增的记录在最前面)和is_deleted = 0(其实这是低级失误),查找SQL需要跳转两次(这还是装了IDE插件的情况),在多表查询时还要编写复杂的ResultMap,体验确实差很多。

记一次IDEA项目报红

今天上午,IDEA突然提示Maven的依赖有更新,点下Enable Auto-Import之后,项目中所有引入的依赖全部报红。
尝试点击Re-Import无效,重装IDEA,删除.idea文件夹,重新Clone项目均无效。

后来同事说远程Maven仓库没法访问,同时在调节设置时发现,在设置中指定了Maven路径(下载公司内部依赖库需要定制的Maven,其实就是改过配置)后,本地Repo路径会自动变成一个不存在的路径,改为~/.m2/repository之后,Re-Import后开始下载jar包,最后恢复正常。

记录一次调通七牛云存储接口的经历

换了新东家,单位配了Mac,由于hexo只会推送生成好的HTML,没有存Markdown源文件,没法写博客(强行给偷懒找借口)。

最近需要同时开多个项目,低压U承受不住,平时操作卡顿,因此换回台式。

新项目中有用到七牛云存储,需求是给存储的图片、视频加上水印,视频需要有缩略图。

于是开始研究七牛云的文档,历时两天终于调通接口,特此记一笔。

调图像水印的接口时遇到的问题是我的低级失误,某个Config类输出七牛云格式的参数时 忘记输出StringBuilder.toString()了,而是用了默认的null。

调视频水印的时候,七牛云有一个坑,调用预处理持久化接口生成带水印的视频,并且使用saveas命令覆盖原视频文件后,无法再用vthumb命令一次性同时创建水印图片,必须请求两次。。

遇到的问题则是由于我忽视了视频转码需要时间的问题,上传成功后在文件管理里没有看到带水印的视频,就以为参数错误,直接把文件删了…

直到我用了七牛云存储实验室,在查询结果时看到了正在转码的信息,才意识到确实需要创建一个私有的多媒体处理队列。

虽说浪费了时间,但是熟悉了七牛云转码和水印相关接口,而且是SpringBoot环境下的应用,算是一段有价值的经历吧。

图解白菜用到的循环队列

实现了用循环队列存QQ消息后,就七八个月没有管(能用的代码才是好代码),以至于写上简历被问到之后支支吾吾说不出……

这个队列比较特殊,只有入队和遍历,没有写出队(业务用不到)。
代码如下(CqMsg为反序列化出来的实体类,记载着QQ号、消息体、发送时间等信息):

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

private int start = 0;
private int end = 0;
private int len = 0;
private int N=100;

private CqMsg[] msgs = new CqMsg[N];
public MsgQueue(){}
public MsgQueue(int N) {
this.N=N;
msgs = new CqMsg[N];
}
public void addMsg(CqMsg msg) {
len++;
if (len >= N) {
len = N;
start++;
}
if (end == N) {
end = 0;
}
if (start == N) {
start = 0;
}
msgs[end] = msg;
this.msg = msg;
end++;
}


public ArrayList<CqMsg> getMsgsByQQ(Long QQ) {
ArrayList<CqMsg> result = new ArrayList<>();
if (start < end) {
for (int i = 0; i < end; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
} else {
for (int i = end; i < msgs.length; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
for (int i = 0; i < start - 1; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
}
return result;

}
}

一开始我创建了一个数组(容量由构造器指定),同时定义两个整数作为坐标变量,再加一个表示队列当前长度的变量,大概是这样的:

1
length = 0
null null null null null null
Start
End

之后每当收到新消息,都将消息插入到end坐标上,之后将end++,同时length++,直到数组装满。

在数组装满后,队列是这样的:

1
length = 5
消息1 消息2 消息3 消息4 消息5 消息6
Start
End

当有下一条消息进入队列时,尝试将队列长度增加:

1
length++

随即满足下方if语句块的判定,将length重置为数组长度,并且将起始点右移:

1
2
3
4
if (len >= N) {
len = N;
start++;
}
1
length = 5

进入下一个if块:

1
2
3
if (end == N) {
end = 0;
}

最后把结果插入到end上,之后end++:

消息7 消息2 消息3 消息4 消息5 消息6
Start
End

不断的插入消息后,Start也会达到数组最右端,此时的队列如图:

消息7 消息8 消息9 消息10 消息11 消息6
Start
End

此时插入消息的逻辑如下:

首先依旧将length重置为当前数组长度;

但是不会进第二个if块,直接进第三个:

1
2
3
if (start == N) {
start = 0;
}

这之后执行end++,会回到数组第一次装满时候队列的样子:

消息7 消息8 消息9 消息10 消息11 消息12
Start
End

每次遍历时,只需要从end→length,再从0→start就行,是一次O(n)的操作。

而插入时,则只需要O(1)(判断几个数字的大小、给数组某个位置赋值)。

如改用纯数组实现,在数组满后,插入消息需要O(n)的时间。

若改用链表,则可以使用迭代器达到与循环队列相同的复杂度:

有新消息时,丢弃头部消息,在尾部追加消息,也是O(1)的操作。

遍历时, 并不使用传统的for+get方法(每次获取链表的某个位置的值代价都是O(n)),而是使用迭代器,可以达到O(n)的时间复杂度。

关于方法重载和覆盖的笔记

最近做笔试题总是遇到覆盖重写,总是记不住,干脆写个笔记辅助记忆。
(不知是我太菜了还是黑皮书的翻译太拗口,本来想着Java语言规范这本书即使看不下去,拿来做工具书查也是好的,结果当工具书也看不懂x)

@Override
方法覆盖存在于子类和父类(接口)之间,遵循以下原则:
方法名字:必须相同
参数列表:必须相同

抛出异常:子类必须<=父类(子类抛出的异常类,必须与父类相同,或者是父类抛出的异常类的子类)
返回类型:子类必须<=父类(子类的返回类型,必须与父类相同,必须是父类的返回类型的子类)

访问权限:子类必须>=父类(例如public不能覆盖为protected)

俗称两同两小一大原则

Overload
方法重写:在一个类中定义多个重名的方法,遵循以下原则:
方法名字:必须相同
参数列表:必须不同,如果参数个数相同,必须保证参数的类型或者顺序不同。
返回类型、访问权限、没有要求。

走进Java并发编程02

第二节:多线程编程的基本需求。

JUC满足了多线程编程的各种需求,但是丰富的需求也是从简单需求开始的。


1.复习:创建线程

我还记得,当年培训班的SE部分结课作业是实现一个Socket客户端/服务端。
其实从这个角度上来说,某鸟的排课水平并不差;这个作业同时要求掌握Socket库的基本用法,还要求理解和实现BIO模型,也就是“服务端监听客户端连接,每个连接创建一个新线程”。

大致的代码如下(节省篇幅,我省略了继承/实现接口的部分,直接使用lambda表达式):
1
2
3
4
5
6
7
8
9
10
11
//略去主类、主方法
//监听10086端口
ServerSocket sc = new ServerSocket(10086);
while(true){
//循环监听
Socket socket = serverSocket.accept();
//为每个连接创建新线程
new Thread(() -> {
//具体的操作,相当于重写run()方法
}).start();
}

这样的代码可能大家都很熟悉(如果觉得陌生的话,也可以改写成一个MyThread类,实现Runnable接口并重写Run方法),而且肯定会有人让我用线程池;还请暂且忍耐一下,看完这些“原始”的代码。

基本需求1:同步与线程安全(synchronized)

问题来了,假设我们要在业务逻辑里对某个东西进行操作,例如……购买商品?

我们设计一个商店类,剩余库存为2,当库存为0时显示已售空,同时让线程睡眠1秒以模拟数据库读写等操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Shop{
private int count = 2;
public void sell(String name){
System.out.println(name+"开始购买商品");
if(count<=0){
System.out.println(name+"发现商品已售空");
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(name+"购买商品完成,剩余库存:"+count);
}
}
}

看起来是不是很合理?

对主方法进行修改,专注于线程而不是Socket:

1
2
3
4
5
6
Shop s = new Shop();
for (int i = 0; i < 5; i++) {
//lambda表达式要求表达式中只有静态变量
int finalI = i;
new Thread(() -> s.sell("顾客" + finalI)).start();
}

我们同时建立5个连接,观察一下控制台,结果是什么?

1
2
3
4
5
6
7
8
9
10
线程0开始购买商品
线程3开始购买商品
线程1开始购买商品
线程2开始购买商品
线程4开始购买商品
线程0购买商品完成,剩余库存:0
线程3购买商品完成,剩余库存:0
线程2购买商品完成,剩余库存:-3
线程1购买商品完成,剩余库存:-3
线程4购买商品完成,剩余库存:-3

这……这是一场灾难!5个买家全都购买成功,而库存变成了-3!

(题外话:在高并发秒杀环境中, count 不再是简单的成员变量,而是缓存/数据库的某个值。万一并发处理错误,导致 count 瞬间变成了负数,这时候如果以 count==0 作为判断条件,会导致秒杀无法停止,所以一定要将判断条件改为小于区间。)

这就是一个典型的线程不安全的类。

定义:线程安全:在单个/多个线程环境下都能得到预期运行结果。

究其原因,是由于线程受到操作系统的调度,我们无法直接控制线程何时运行,即使是调节优先级,得到的也只是影响,而不是保证(可以试试把五个线程的优先级排一下看看结果)!

幸运的是,Java语言提供了同步关键字 synchronized ,它是Java对多种锁的封装,根据使用情况不同有不同的表现。

我们把它加到 sell 方法上……

1
2
3
4
5
6
7
8
9
10
线程0开始购买商品
线程0购买商品完成,剩余库存:1
线程3开始购买商品
线程3购买商品完成,剩余库存:0
线程4开始购买商品
线程4发现商品已售空
线程2开始购买商品
线程2发现商品已售空
线程1开始购买商品
线程1发现商品已售空

结论:在将 synchronize 关键字加到某个方法上后,我们可以确保在一个线程进入 这个对象 的这个方法之后,就不会有另一个线程也进入,避免了超售的情况。

这背后实际是线程获取了 这个对象 的锁,在执行完方法后,自动释放了 这个对象 的锁,是不是很智能?

与此相同的用法还有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public void sell(String name){
synchronized (this){
System.out.println(name+"开始购买商品");
if(count<=0){
System.out.println(name+"发现商品已售空");
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(name+"购买商品完成,剩余库存:"+count);
}
}
}

在这段代码里, this 关键字指向了当前的对象,与在方法上加 synchronize 关键字作用是一样的。

所以明确一个概念: synchronize 代码块锁住的不是代码块,而是 synchronize 后面圆括号中的对象!

定义:对象锁: 让同一个对象的某个方法无法被多个线程并发执行的机制。


题外话:

为什么我要强调同一对象呢?

让我修改一下代码,每次创建线程都创建一个 Shop 对象……

1
2
3
4
5
6
//主方法、主类
for (int i = 0; i < 5; i++) {
//lambda表达式要求表达式中只有静态变量
int finalI = i;
new Thread(() -> new Shop().sell("顾客" + finalI)).start();
}

然后把Shop对象的库存属性改为静态变量,以使所有Shop对象可以共享它。

1
2
3
4
class Shop{
private static int count = 2;
...
}

点击运行,灾难又出现了!

现在的编码需求,变成了让 所有Shop类的对象sell() 方法都无法被同时执行。

继续修改 sell() 方法,既然 count 已经是静态变量,那么我们为什么不把 sell() 方法也改成静态方法呢?

这时候IDE报了一个错,原来在静态方法中不能用 this 关键字(对象都不一定有, this 指定谁去?),那么把它改为 Shop.Class ,类对象就不是对象了?

现在的 sell() 方法:

1
2
3
4
5
6
public static void sell(String name){
synchronized (Shop.class){
....
}
}
}

当然,我们也可以直接把 synchronize 关键字丢回到静态方法上:

1
2
3
public synchronized static void sell(String name){
...
}

定义:类锁: 让同一个类 多个实例对象 的某个方法,都 无法被多个线程并发执行的机制。


基本需求2:线程间通信(Object.wait()/Object.notify())

刚刚我们实现了一家有序出售物品的商店,但是一家商店不能只能出售物品,卖光了怎么办呢?进货。

大致的过程是,当某个顾客线程发现 count<=0 时,挂起所有“顾客”线程,并且通知一个线程去进货,等待进货完成后给所有挂起的“顾客”线程发送通知。

首先我们要在主方法中,单独开启一个进货线程(这个写法是lambda表达式中的方法引用,由于该线程的run方法只执行这一个无参方法,被IDE检测到了提示替换):

1
new Thread(s::purchase).start();

仅仅 synchronized 关键字已经不够用,我们给 Shop 类增加一个字段:

1
private final Object myLock = new Object();

这个字段没有其他意义,仅仅作为一把被别人持有的对象锁而存在。

和上一章不同, 这把对象锁的目的,不再是让这个对象的方法无法被并发执行,而是让其他线程持有它,以便唤醒或挂起这些线程。

注意:

虽然我这里用了 “唤醒”和“挂起”,但我指的并不是 Thread.suspend()Thread.resume()

这一对被废弃了十几年的方法,是属于 Thread 类的,调用 suspend() 在挂起时并不释放这个线程持有的锁,因此极其容易引发死锁;

Object.wait() 会让所有持有这个对象的对象锁的线程阻塞,同时也停止持有这个对象的对象锁。

当调用 Object.notify() 时,会随机取出一个因为 wait() 方法阻塞的线程,让它继续运行的同时重新持有对象锁;

而调用 Object.notifyAll() 时,会让所有之前因为 wait() 方法阻塞的线程解除阻塞,但是注意:只有那个重新持有对象锁的线程才能继续运行。

明白了 wait()notify() 这一对方法后,我们来着手改写 sell() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
while (count <= 0) {
try {
System.out.println(LocalTime.now() + name + "要求了进货");
synchronized (myLock) {
myLock.notify();
}
System.out.println(LocalTime.now() + name + "进货等待中");
this.wait();

} catch (InterruptedException e) {
e.printStackTrace();
}
}
...

在现在的 sell() 方法中,首先把库存不足的判断由 if 改为 while ,这样每个顾客在收到进货完成的通知后,都会重复检查一次库存。

接下来改写条件块的内容:

  • 尝试获取myLock的对象锁;

  • 在其他获取了myLock对象锁,并且被阻塞的线程中,选一个恢复运行(在主方法中实际我们只创建了一个这样的线程,因此这里 notify()notifyAll() 没有什么区别 );

  • 将所有持有当前对象的对象锁的线程阻塞。

接下来是进货方法:

  • 当然进货方法要写死循环,一旦被 sell() 方法恢复运行后,能再次阻塞,等待下一次需要进货的时候;

  • 获取myLock的对象锁,并且开始阻塞;

  • 在被 sell() 方法恢复运行后,将库存+5,然后将所有获取了当前对象的对象锁,并且被阻塞的线程恢复运行(招呼其他顾客继续购物)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 public void purchase() {
while (true) {
synchronized (myLock) {
try {
System.out.println(LocalTime.now()+"店家等待进货通知");
myLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(LocalTime.now()+"店家开始进货");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count += 5;
synchronized (this) {
this.notifyAll();
}
System.out.println(LocalTime.now()+"店家进货完成");
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
17:12:32.563顾客0开始购买商品
17:12:32.563店家等待进货通知
17:12:33.564顾客0购买商品完成,剩余库存:1
17:12:33.564顾客4开始购买商品
17:12:34.564顾客4购买商品完成,剩余库存:0
17:12:34.564顾客3开始购买商品
17:12:34.564顾客3要求了进货
17:12:34.564顾客3进货等待中
17:12:34.564店家开始进货
17:12:34.565顾客2开始购买商品
17:12:34.565顾客2要求了进货
17:12:34.565顾客2进货等待中
17:12:34.565顾客1开始购买商品
17:12:34.565顾客1要求了进货
17:12:34.565顾客1进货等待中
17:12:36.565店家进货完成
17:12:36.565店家等待进货通知
17:12:37.566顾客1购买商品完成,剩余库存:4
17:12:38.566顾客2购买商品完成,剩余库存:3
17:12:39.567顾客3购买商品完成,剩余库存:2

我们圆满的完成了需求,尽管这个程序会有一个一直等待是否去进货的店家,所以不会直接结束。

基本需求3:线程间通信2(Thread.join())

还是刚才的问题,我们让店家去进货,但是我们不希望多加一个对象,然后折腾当前对象/myLock这两个对象的锁。

我们把在主方法中创建进货线程、并且循环阻塞等待通知,改成在 sell() 方法中创建进货线程、并调用 join() 方法。

顾名思义, join() 方法表示立即阻塞当前线程,并且让被调用 join() 方法的线程“参与”到程序执行中,在被调用 join() 方法的线程执行完后,才恢复之前阻塞的当前进程的运行。

注意, join() 方法必须在 start() 方法调用后调用;如果 join() 方法和 start() 方法中有其他代码, join() 方法会优先执行。

同时我们去掉 purchase() 方法中关于对象锁的语句:

sell() 方法:

1
2
3
4
5
6
7
8
9
10
11
while (count <= 0) {
try {
System.out.println(LocalTime.now() + name + "要求了进货");
Thread a = new Thread(this::purchase);
a.start();
a.join();
System.out.println(LocalTime.now() + name + "进货等待中");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

purchase() 方法现在只剩下了操作 count 以及一些提示:

1
2
3
4
5
6
7
8
9
10
public void purchase() {
System.out.println(LocalTime.now() + "店家开始进货");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count += 5;
System.out.println(LocalTime.now() + "店家进货完成");
}

运行,这次程序执行完自动结束了,因为不再有一个后台持续阻塞的线程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
17:54:09.025顾客1开始购买商品
17:54:10.027顾客1购买商品完成,剩余库存:1
17:54:10.027顾客4开始购买商品
17:54:11.027顾客4购买商品完成,剩余库存:0
17:54:11.027顾客3开始购买商品
17:54:11.028顾客3要求了进货
17:54:11.031店家开始进货
17:54:13.032店家进货完成
17:54:13.032顾客3进货等待中
17:54:14.033顾客3购买商品完成,剩余库存:4
17:54:14.033顾客2开始购买商品
17:54:15.033顾客2购买商品完成,剩余库存:3
17:54:15.034顾客0开始购买商品
17:54:16.034顾客0购买商品完成,剩余库存:2

对Java库源码有过分析的可能会知道, join() 方法内部其实是由 Object.wait()/Object.notifyAll() 实现的!

附:本章完整代码:

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
package top.mothership;

import java.time.LocalTime;

public class Main {
public static void main(String[] args) {
Shop s = new Shop();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> s.sell("顾客" + finalI)).start();
}
}

}

class Shop {
private int count = 2;


public synchronized void sell(String name) {
System.out.println(LocalTime.now() + name + "开始购买商品");
if (name.contains("2") || name.contains("0")) {
Thread.yield();
}
while (count <= 0) {
try {
System.out.println(LocalTime.now() + name + "要求了进货");
Thread a = new Thread(this::purchase);
a.start();
a.join();
System.out.println(LocalTime.now() + name + "进货等待中");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(LocalTime.now() + name + "购买商品完成,剩余库存:" + count);
}

public void purchase() {
System.out.println(LocalTime.now() + "店家开始进货");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count += 5;

System.out.println(LocalTime.now() + "店家进货完成");
}
}

转:知乎某关于Java技能树的回答

昨天在知乎看到一个回答,大意是:如果我是面试官,就从Object类的方法问起。

clone方法可以问深复制浅复制,接下来可以涉及引用类型和值类型,自动装箱拆箱,类初始化顺序……
equals方法可以问Integer的缓存,JVM内存模型,然后转到hashCode;
finalize方法可以问作用、缺点,转到垃圾回收,GC的算法、实现、内存模型……
getClass方法当然问反射,接下来可以拓展到Spring,扩展到动态代理 aop……
hashCode可以考基本算法,HashTable和HashMap,是否线程同步,然后可以考并发容器……
wait/notify/notifyAll引申出多线程,接下来就是并发包中的各种调度器线程池……

翻了翻链接,原回答:你遇到过哪些质量很高的 Java 面试? - 知乎
https://www.zhihu.com/question/60949531/answer/182146087

走进Java并发编程01

前言

本系列文章作为学习笔记,记录了博主向并发包进攻的过程。

在此,我假设本文读者已经掌握了Java的基本编码,如果是大学生,至少Java的课程要通过;如果是培训学员,至少需要学完SE部分。虽然影响不大,但是如果对Spring,或者至少Java Web有一定了解和编码经验,应该会对阅读有所帮助(我可能会举一些网站相关的例子)。

本文夹杂大量个人理解,错误不可避免,非常欢迎评论指正。

正文

第一章 :假如没有juc……

2004年9月30日18:00PM,J2SE1.5发布,成为Java语言发展史上的又一里程碑。为了表示该版本的重要性,J2SE1.5更名为Java SE 5.0。——via 维基百科

前置知识:JSR——Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。

JSR166完成(同样也是J2SE1.5发布)之后,Java程序员发现,除了Thread类、Runnable接口外,Java引入了大量的API,极大的丰富了Java在多线程编程的能力。

让我们从已掌握的知识开始,先忘掉java.util.concurrent,复习一下基础的概念:线程/进程/并发/并行,还有我们的老朋友Thread/Runnable/synchronized/volatile。


第一节:进程与线程。

在提到线程和进程之前,推荐一篇有趣的文章:
《我是一个CPU:这个世界慢!死!了! 》
(阅读前需要对计算机的基本组成有一定了解,至少要明白SSD、CPU的L1L2缓存是什么。)

如果将一颗2.6GHz频率,一个逻辑核心的CPU(这个性能在现在可以说是很低了,主流的i7-8700K有6个物理核心,通过Intel的超线程技术让系统认为有12个逻辑核心,最高睿频4.7GHz)执行每个指令的时间放大到一秒,那么从内存中读取1MB 需要7.5天,从SSD上的随机读取需要用4.5天,从SSD读取1MB需要一个月,而HDD的磁盘寻址需要10个月,连续读取1MB需要20个月!

我们设想一个场景。你打开一台一个逻辑核心的电脑,登录QQ,同时打开IDEA开始编写代码,在你眼里QQ和IDEA是同时运行着的程序。但是在CPU的眼里,QQ和IDEA需要CPU来执行的操作其实是轮流着来的,只是它切换的速度太快,你感受不到!

CPU其实一直不断的在加载QQ的上下文(内存、显卡、硬盘等资源)→执行QQ→保存QQ的上下文→加载IDEA的上下文…

好,我们引申出概念:

1.什么是进程?

“计算机中已运行程序的实体,分配资源的基本单位。”

我们编写的程序,只是指令(比如做加法)、数据(比如一个整数1)和组织形式(比如某个数组,是按顺序排的一系列元素)的描述,当我们下达运行它的命令时,才会产生进程。操作系统按进程为单位分配资源,这些资源包括一片内存、操作系统描述符(所谓文件句柄/文件描述符),安全特性以及CPU状态(视是否在运行存储在寄存器/内存中)等。

总结:进程是一个单位,我们下达运行程序命令时,就可以向操作系统申请领取“一个进程的”大礼包,包含了一段时间内的一部分计算机资源。

2.什么是线程?

“操作系统调度的最小单位”。
在此不对OS这门课做展开,不讨论内核态用户态线程。

我们再设想一个场景。你从操作系统领取了大礼包,它包含了一部分的时间(我感觉生命在流逝)和一部分的计算机资源。但是现在你的代码里需要打开硬盘上的某个文件,或者更过分的是等待某个网络请求(还记得150ms在CPU的眼里是12.5年吗?)……

这是一种严重的浪费!在这个礼包、甚至之后的好多礼包给的时间内,你创建的进程什么都没有做,白白的浪费了系统资源,时间一到CPU就切换到了别的程序,这极大的影响了程序的执行效率。

这时候如果我能把进程拆分为一个个部分,由它们共享这些资源;当第一个部分需要等待某个耗时较长的操作时,其他部分也可以在这份时间里利用这份资源,岂不美哉?

总结:线程是一个更小的单位,“一个进程”的资源,可以由多个“线程”共享,免得出现资源浪费。

3.什么是并发,什么是并行?

Erlang之父简单的介绍了它们的区别:

串行是一个队列一台咖啡机,如果有人匹到了一把Dota,过了一个小时打完回来接咖啡,后面的人都得必须等着;
并发是两个队列用一台咖啡机,咖啡机在处理两个队列的人的状态中不断切换,在逻辑上这台咖啡机可以同时处理两个队列;
并行则是两个队列用两台咖啡机,有两台咖啡机在同一个时间点处理两个队列,注意,是真正意义上、物理上的同时,这也是多逻辑核心CPU的模型!

白菜日记6

快半年了,白菜的功能也发生了很多变化。
加入了对接osu search接口,可以提供搜索词来找到自己在某个图的成绩。
加入了其他三个模式的支持(懒得从其他语言移植其他模式的PP计算)。
另外还有cost和bounspp的计算……
记不太清这五个月做了什么了,现在有了AOP的异常通知、参数拦截,redis缓存……
现在说起来好像挺简单,不过实现的时候还是挺开心的。

比赛分析也改成了用命令增删玩家、谱面……

关于finalize(),System.gc()和C#、C++的析构函数的一点笔记

2018-4-2 补充:

在阅读了深入理解JVM后,作者的这一段话彻底理清了这一块:
“需要说明的是,上面关于对象死亡时finalize()方法的描述(前文描述了在finalize()方法中让对象重新被引用,从而可以避免一次垃圾回收)可能带有悲情的艺术色彩,…笔者建议大家避免使用它,…而是Java刚诞生时为了使C/C++程序员更容易接受它做出的妥协。它运行代价高昂,不确定性太大,无法保证各个对象的调用顺序,有些教材描述它适合用来关闭外部资源,这完全是对这个方法用途的一种自我安慰,finalize()能做的,finally块等其他方式能做的更好、更及时,所以笔者建议大家完全可以忘掉Java中有这个方法的存在。”

这个方法实质是一种妥协,缺点见下文。

先说finalize。

在我之前的个人理解里,finalize方法并不会直接导致该对象被释放/回收;因此它不是C++意义上的析构函数。
(事实上析构函数的作用是定义对象被释放时候的行为,是对析构函数的调用,而不是析构函数本身导致对象被释放)
它用于 标记在对象被回收时应该做一些事情;例如释放其他资源,关闭连接等。

以下说明摘抄自1.6的API文档:

当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
finalize 的常规协定是:当 Java虚拟机已确定尚未终止的任何线程无法再通过任何方法访问此对象时,将调用此方法,除非由于准备终止的其他某个对象或类的终结操作执行了某个操作。finalize 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作。例如,表示输入/输出连接的对象的 finalize 方法可执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。
Object 类的 finalize 方法执行非特殊性操作;它仅执行一些常规返回。Object 的子类可以重写此定义。
Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法。但可以保证在调用 finalize 时,调用 finalize 的线程将不会持有任何用户可见的同步锁定。如果 finalize 方法抛出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。
在启用某个对象的 finalize 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。
对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。
finalize 方法抛出的任何异常都会导致此对象的终结操作停止,但可以通过其他方法忽略它。


JDK9的API中,该方法已经被废弃:

已过时。 finalize机制本质上是有问题的。finalize可能导致性能问题,死锁和挂起。
finalize方法中的错误可能导致资源泄漏; 如果 finalization不再需要,无法取消 ; 并且在不同对象的finalize方法的调用中没有指定排序。 此外,finalization的时间并不能得到保证。finalize方法可能只能在无限期的延迟之后,才调用到可终结的对象上。
如果一个类的实例持有非堆资源,那么它应提供一种方法,来实现这些资源的显式释放,如果适用,它们还应实现AutoCloseable 。
Cleaner和PhantomReference在对象变得不可达时提供更灵活和更有效的方式来释放资源。

API Note:
嵌入非堆资源的类,具有许多清除这些资源的选项。 该类必须确保每个实例的生命周期 比其嵌入的任何资源的寿命都要长。
当嵌入在对象中的资源正在使用时,可以使用Reference.reachabilityFence(java.lang.Object)来确保对象保持可访问。

一个子类应该避免覆盖finalize方法,除非子类嵌入在收集实例之前必须清理的非堆资源。
与构造函数不同,调用子类的finalize()不会自动调用父类的finalize()方法。如果一个子类覆盖了finalize,必须明确地调用超类终结器。
为了防止异常提前终止终结链,子类应该使用一个try-finally块来确保总是调用super.finalize() 。 例如:

1
2
3
4
5
6
7
8
@Override 
protected void finalize() throws Throwable {
try {
... // cleanup subclass state
} finally {
super.finalize();
}
}

结论:在1.9之后,我们不应该用这个方法来实现 让一个类被回收时做某些事 的功能。如果需要做某些事,应当显式的提供做这些事的方法,例如实现AutoCloseable接口,用虚引用和Cleaner等新版的功能。

再说System.gc()。

从入门开始,我便被各种书籍告知,不要手动调用System.gc()。那么这个方法到底做了什么呢?是立即发起垃圾回收,还是催促虚拟机进行一次垃圾回收呢?
看看API文档:

调用gc方法表明,Java虚拟机花费了回收未使用对象的努力,以使其当前占用的内存可用于快速重用。
当控件从方法调用返回时,Java虚拟机已经尽力从所有丢弃的对象中回收空间。

不知道具体JVM的实现是什么,至少这段话我的理解就是 立即发起一次垃圾回收……
由于垃圾回收会停止主程序活动,而且的确实际开发中也没有理由手动发起垃圾回收,总之还是不要用的比较好(

联动:C#的析构函数

学习finalize的时候,往往会同时提起析构函数这一C系语言的产物。
但是在C#中,析构函数和finalize的作用,在我的理解中是一致的:即 让这个类在被回收前做一些事情。

不过C#的析构函数并没有被废弃,微软同时建议使用IDispose接口,实现Dispose()方法。
不过和Java的AutoCloseable不同,Dispose方法需要被重载两次,第一次是表示该对象不执行析构函数:

1
2
3
4
5
6
7
public void Dispose()
{
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}

第二次才是真正的执行逻辑,此时的重载是Dispose(bool),如果对它方法的调用是来自Dispose(),则参数为true;如果来自析构函数,则参数为false。因此需要对参数进行判断:当调用来自Dispose()时,释放非托管资源。

联动:C++的析构函数

水平有限,不做拓展.
大致可以看出也是在对象生命周期结束时被自动调用,但是由于没有自动内存管理,C++的析构函数会在如下几种情况被调用:

  • 使用 delete 运算符 显式解除 分配了使用 new 运算符分配的对象。 使用 delete 运算符解除分配对象时,将为“大多数派生对象” 或 “属于完整对象,但不是表示基类的子对象的对象”释放内存。 此“大多数派生对象”解除分配一定仅对虚拟析构函数有效。 在类型信息与实际对象的基础类型不匹配的多重继承情况下,取消分配可能失败。
  • 具有块范围的本地(自动)对象超出范围。
  • 临时对象的生存期结束。
  • 程序结束,并且存在全局或静态对象。
  • 使用析构函数的完全限定名显式调用了析构函数。

白菜日记5

之前的名字好像太长了(

自动清理文件我直接写死了Linux的路径,反正也不会在开发环境下用到这个功能(
今天折腾了个验证码,初步看了一下Interceptor和Filter的区别,前者是基于AOP来拦截每个Controller请求,本质上是个Aspect,和我用来全局捕捉异常的东西是一样的。。
后者则是J2EE里Servlet的功能,在Spring的DispatcherServlet之前工作,在Controller工作之前就拦截了请求……
虽然其实功能差不多,传入的都是HttpServletRequest/Response,不过我的代码要用到redis,而redis的工具类是托管在Spring里的,所以我还是选了Interceptor。

这个比赛分析功能还是挺有用的……后期考虑改成命令(

晚点改一下分数抓取的目标和继续做登录吧(

白菜的开发日记4

为了节省费用,我尝试将白菜移植到一台必须有Linux环境的机子上运行(为了和节点共存)。
一开始打算使用WSL,无奈这东西在桌面Win10已经普及了,但是Server系统下只有一个Insider Preview。

Preview到什么程度呢,那个系统没有Explorer,甚至没办法移植(comctl32.dll还是哪个系统文件是定制的,没法覆盖也没法兼容,毕竟VPS可没有PE和安全模式)。

在一番挣扎之后我们彻底放弃了它,改为Ubuntu,于是我的灾难来了……

在此之前我也曾经在CentOS等系统下配过Java+MySQL的环境,但是酷Q是用易语言写的,于是就涉及到一个Docker的使用问题。

在坑了一天之后(其实是我耐不下心去啃文档,只能自己瞎几把试),大概了解了这点:
docker pull是下载一个镜像,类似于创建一个类
docker run是创造一个容器,类似于实例化对象
而docker start和stop是操作这个容器,docker rm是删除容器(对象),docker rmi是删除镜像(类)。

大概遇到的坑有:
在docker内运行的酷Q,虽然能提供5700的HTTP Server,但是同一台机子上运行的Tomcat开放的8080端口它却无法作为客户端使用,
指定localhost:8080和127.0.0.1:8080都无效,必须把酷Q的消息上报地址改为服务器的公网IP(我实际上是改为了域名)

我的代码里需要处理ppy给的类似于 1970-01-01 00:00:00的日期格式,
在Win下面,Gson默认转换没有问题,但是Ubuntu下就不行(似乎和时区无关,我本地的WSL也是这样),必须硬编码一个日期格式才能起作用。

这次重构,把CQService里具体的处理拆出来一个CmdUtil,实体类、util全部分包处理,
为了避免Win/Ubuntu的路径不一致将所有素材以二进制形式存到了数据库……四个语音base64编码之后直接硬编码到程序里(

同时还发现,我之前给ImgUtil加了一个static的Map,然后写了个静态方法去用NIO爬目录,把图片塞到那个Image里,最后在构造器里调用这个方法……
但是我ImgUtil是用prototype模式注入,每次有HTTP连接都会实例化一个新的ImgUtil,重新爬一次,压根没起到缓存的作用_(:з」∠)_

还把oppai换成了Java实现,那作者自称不喜欢Java,也别问他要更多Java程序或者支持,虽然oppai的功能全部搬了过去。
用了一下,我发现他用了一大堆静态内部类,而且要使用它 必须把它和调用它的类放在同一个包里(有个关键东西的构造函数是包访问权限的)。
本来想自己改造,但是想到万一PP算法一改,作者一更新那我不就歇逼了?于是老老实实按他规定的方法来_(:з」∠)_

一开始没掌握正确的使用方法,于是昨晚上漏掉了HR和DT(我把MOD应用之前的star穿进去了,柑橘妖怪作者不提供详细的example)。

再一次体会到了“你以为你写的代码是这样跑的,其实根本不是”的心情。

接下来就是写网站部分,然后还有一个准备咕很久的东西……还有白菜的邀请入群机制也要改改,自动清理文件的机制也要改,不过这两天我有点心力憔悴,先咕着吧……

白菜的开发日记3

许久没更博客了,写点白菜最近的进度吧。

整个Web版重构完成,我还在路由器上搭了一个mysql server,然后用mysqldump每天凌晨备份白菜的数据库。

顺手加上了凌晨把获取失败的ID以邮件形式发到自己邮箱,还加了清空当日生成的临时文件。

弱智啪啪啪提了一个req,!sleep 按小时为单位禁言自己,没想到做出来真的一堆弱智用……

日记2里提到的绘图类,被我从静态代码块改成了静态方法,然后在!sudo bg命令时调用它(否则会导致修改用户bg不能马上生效)。

顺手做了一个!fp功能,不过mp4和5没什么人用……

我还研究了一下HTTP Client,做了模拟登陆osu并且下图,还写了解析.osu的正则表达式,来从官网获取BG。

甚至还处理了0,0,”sb\bg.png”这种情况……

在这里吹自己一波,我居然想到了把官网下图时的InputStream包装为ZIPInputStream,直接在内存中解压osz……

写完才发现“卧槽我居然真的把这个想法实现了”。。可以说白菜现在的粗壮性又上了一个台阶(

Java中的值传递和引用传递

今天突然提到Java传递变量的方式,研究了一下值传递和引用传递。

我的结论是:Java是以值传递的方式传递对象的引用/基本数据类型本身。

可能听起来有点拗口,首先明确一下值传递和引用传递。

值传递,是在方法内无法通过赋值等手段改变这个变量本身,因为传递过程中这个变量被复制了一份传入方法内,方法内对这个形参的操作并不会影响方法外。

而引用传递,则是将对象本身的内存地址传入方法内,方法内操作这个变量会对这个内存地址上这个变量的本体进行操作。

首先Java的方法里,用赋值等号=是改变不了调用者里形参的值的,因此传递的方式是值传递无误。

当然,如果调用这个对象本身的方法去改变自身(例如StringBuilder的append方法,或者是Javabean的Setter方法)是可以做到的,因为这个引用依然引用着这个对象,只是这个对象本身变了而已。

所以给数组的某个索引赋值之后,是可以改变数组本身,因为数组本身也是对象,继承自Object……只不过是一个特殊的对象,可以参考
http://www.blogjava.net/flysky19/articles/92763.html?opt=admin
数组的类是运行时生成的,它没有构造方法。证明数组是对象的最简单方法是:用等号赋值时并没有克隆一份数组,而只是重新指定了引用,要克隆数组得用System.arraycopy()方法。

所以手写排序时递归调用本体,并不会内存爆炸,因为数组本身并没有被复制。

(以上仅仅是个人看法,欢迎指正)

白菜的开发日记2

白菜之前的代码基本已经稳定运行了,于是我准备把它改成一个Web项目,采用HTTP API来收发消息,同时将来可能扩展出网页什么的……

在重构的时候遇到一个问题:之前我在绘图类,用nio扫出结算界面需要的所有图片,然后用static代码块包裹:

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
static {
final Path resultPath = Paths.get(rb.getString("path") + "\\data\\image\\resource\\result");
//使用NIO扫描文件夹
final List<File> resultFiles = new ArrayList<>();
Images = new ArrayList<>();
Nums = new ArrayList<>();
Mods = new ArrayList<>();
SimpleFileVisitor<Path> resultFinder = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
resultFiles.add(file.toFile());
return super.visitFile(file, attrs);
}
};

try {
//将所有文件分为三个List
java.nio.file.Files.walkFileTree(resultPath, resultFinder);
for (int i = 0; i < 23; i++) {
Images.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 23; i < 37; i++) {
Nums.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 37; i < 48; i++) {
Mods.add(ImageIO.read(resultFiles.get(i)));
}
zPP = ImageIO.read(resultFiles.get(48));
zPPTrick = ImageIO.read(resultFiles.get(49));
} catch (IOException e) {
logger.error("读取result相关资源失败");
logger.error(e.getMessage());
}

这样这些对象只会在这个类第一次被加载的时候生成,避免了每次绘图重复扫描文件夹,降低效率的问题。

然后后面画图的时候,我只需要调用List中的BufferedImage对象

1
2
3
//右下角两个FPS
g2.drawImage(Images.get(1), 1300, 699, null);
g2.drawImage(Images.get(2), 1300, 723, null);

但是如果我要新加功能就会显得不便:我只能添加z开头的文件名,否则我得把所有的Images.get()后面的数字调一下。。

看了一眼它们的文件名,我觉得ppy应该是在osu启动的时候把皮肤全部加载到内存,然后根据文件名绘制……

看了一下nio,好像能生成文件名,那我把List换成Map好了……

白菜的开发日记1

emm。从16号早上出门前Initial Commit,到23号中午12点白菜正式上线,这一周的时间过的真是快啊……

https://github.com/Arsenolite/osubot/commits/master

43次commit之后,终于有时间坐下来写点开发日记了。严格的说这是第一个我写出来能用的程序啊(x

先介绍一下她吧,她是一个面向osu群的QQ机器人,具体用法可以参见github的readme(普通用户就两条指令你还写个readme真是凑不要脸x)

相对别的机器人的亮点就是她返回的是图片,样式主要由啊哇设计,我们从旁修改,当然最后是我实现的。(记得18号那天我画一行丢一个图在群里x

作为Java程序员,老本行就是折腾数据库,当然白菜也有这方面的功能。每天凌晨4点将所有登记了的玩家的数据爬取,然后存入,提供一个对比功能。

18号晚上写完用户名片绘制,19号上午写完凌晨的定时任务,20号做了一些sudo命令。

22号凌晨1点终于搞定BP绘制,然后白天研究maven打包,开通vps,关闭mysql占用空闲内存缓存等等。

22号晚上怕他突然提出来一个大BUG,之前数据库中我存入的是用户名,但是这样会导致无法识别改名玩家的问题。

于是晚上赶工重构代码+改表结构,由于发现操作系统的时区就是北京时间,而BP返回时间是根据API key对应玩家的国籍来的(也是北京时间),砍掉了所有的时区转换。

在vps上运行的时候查API失败率非常低,于是将api工具类中加上重试机制,砍掉了其他地方代码对网络错误的try-catch。

然后就是些小bug(之前改的时候没改彻底导致的各种问题),昨天中午正式上线,下午晚上研究了jsoup爬取网页指定元素,又把爬score rank改成了二分法。

顺手写了个自动欢迎新人,由于啊哇还没想好scorerank的呈现方式,把绘制scorerank的代码注释掉发布到了服务器上。

……挺后悔没有边开发边写博文的,现在有的遇到的bug都已经忘记了……

总之这个项目增强了我对SQL、前端(jsoup提供的是js的getElementById方法,和css的div.Class风格的选择器)的熟练度,顺便还达成了第一次使用WinServer系统的成就,勉强算是linux+WinServer都能干的运维?

复习了以前练手搞的多线程机制,IO流,各种包装类,日期处理,字符串处理之类的基础问题,初次尝试手写二分法(特别新鲜),可以说它弥补了我Java基础代码写得少的缺陷……

毕竟之前在单位搞的SSM框架更多是做填空题,把XML配好,crud和参数验证写好,甚至那个小项目都用不着组装复杂pojo……到处都是提供好的最佳实践(

……虽然这个项目用的也都是给好的类和方法,充其量起到了熟悉JDK本身的作用,毕竟不是和c一样要手写String的查找替换啥的(x

emm,白菜大概是不会弃坑的,以后尽量写出详细的遇到/解决bug的过程吧233

记一次在CentOS 7上部署Java测试环境的过程

将之前配置LNMJ测试环境的过程归个档,方便日后查阅。

1) JDK环境

下载并安装
1
2
cd usr
mkdir java

当时不小心下到了JDKDemo的包,直接下JDK包时发现下载下来的是网页,于是只好手动下载再上传。

1
2
cd java
rpm -ivh jdk-8u131-linux-x64.rpm
配置环境
1
vi /etc/profile

按I进入编辑模式,将如下内容加入profile中:

1
2
export JAVA_HOME=/usr/java/jdk1.8.0_131
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

输入

1
source /etc/profile

命令,让配置生效。
最后和Windows平台一样,输入java -version来验证安装。

2)Tomcat的配置

下载并安装

wget下载安装包,并且解压。

1
tar -zxvf apache-tomcat-x.x.xx.tar.gz

修改文件夹名字方便后续使用。

配置环境变量

编辑/etc/profile,在末尾加上:

1
2
3
export CATALINA_HOME=/usr/apache-tomcat
export CATALINA_BASE=/usr/apache-tomcat
export PATH=$PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin

刷新并运行Tomcat进行测试:

1
2
source /etc/profile
/usr/apache-tomcat/bin/startup.sh
将Tomcat配置为系统服务,方便用systemctrl管理:
  • 在Tomcat的bin目录下,新建setenv.sh脚本。Tomcat启动时会自动运行这个脚本。
    1
    CATALINA_PID="$CATALINA_BASE/tomcat.pid"
  • 使用vi编辑/usr/lib/systemd/system/tomcat.service文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [Unit]
    Description=Tomcat
    After=syslog.target network.target remote-fs.target nss-lookup.target
    [Service]
    Type=forking
    PIDFile=/usr/apache-tomcat/tomcat.pid
    ExecStart=/usr/apache-tomcat/bin/startup.sh
    ExecReload=/bin/kill -s HUP $MAINPID
    ExecStop=/bin/kill -s QUIT $MAINPID
    PrivateTmp=true
    [Install]
    WantedBy=multi-user.target

然后就可以用systemctrl命令来管理Tomcat的开机启动等问题了。

当时出了一个很奇怪的bug,我本地运行没有问题的war包,上传到服务器上就是404,在Tomcat管理页面里也能看到这个WebAPP在运行。
最后我直接删掉了服务器Tomcat,将本地Tomcat直接上传上去,并把权限改为0777,最终解决问题。

3)MySQL

MySQL这事比较复杂,Oracle收购了MySQL之后,准备在MySQL6.x收费,然后社区开发了一个叫MariaDB的分支,采用GPL授权,以此应对。
当然我还是不准备采用这个分支,能求稳就别浪……

下载并安装
1
2
3
wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
rpm -ivh mysql-community-release-el7-5.noarch.rpm
yum install mysql-community-server
启动

运行mysql -u root启动。
(值得注意的是,只有root账户没有密码的时候才能这么启动,否则返回Access denied,不会提示你输入密码。在有密码的时候需要加上-p参数。)
进入mysql命令行之后,运行SQL语句设置密码:

1
set password for 'root'@'localhost' =password('密码');

当时配置完成后死活连不上,最后发现CentOS不是用IPTABLE作为防火墙,而是使用了firewall。
更换为IPTABLE,并且开放端口后问题解决。

一些配置:
  • 更改编码:
    1
    vi /etc/my.cnf
    在文件末尾添加:
    1
    2
    [mysql]
    default-character-set =utf8
  • 设置其他IP可以连接:
    在MySQL命令行中执行:
    1
    grant all privileges on *.* to root@'%'identified by '你的密码';

4)nginx

nginx的安装给我一种印象,下载的似乎是源代码,还要下一个gcc的编译器,现场编译现场用。
(不知道是不是真的,如有谬误请指正)

安装依赖
1
2
3
4
yum install gcc-c++  
yum install pcre pcre-devel
yum install zlib zlib-devel
yum install openssl openssl--devel
安装nginx本体
1
2
wget http://nginx.org/download/nginx-1.7.12.tar.gz
tar -zxvf nginx-1.7.12.tar.gz

并重命名目录,去掉版本号,方便后续使用。
当时安装完后,因为firewall的问题(我看错,认为80端口是打开的)以为配置出了错,最后一怒之下关了防火墙一切正常了。
决定换回熟悉的iptable。
#####配置nginx代理Tomcat
编辑nginx.conf文件,在http-server-location-proxy_pass段中填入:

1
2
http://127.0.0.1:8080
#此处8080为Tomcat默认端口号
将nginx配置为服务
  • 先编辑nginx.conf文件:
    1
    vi /usr/local/ngnix/conf/nginx.conf
    将里面的pid段后面的路径复制出来。
  • 使用vi编辑/usr/lib/systemd/system/nginx.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    [Unit]
    Description=nginx - high performance web server
    Documentation=http://nginx.org/en/docs/
    After=network.target remote-fs.target nss-lookup.target
    [Service]
    Type=forking
    #与nginx.conf一致
    PIDFile=/usr/local/nginx/logs/nginx.pid
    #启动前检测配置文件 是否正确
    ExecStartPre=/usr/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
    #启动
    ExecStart=/usr/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
    #重启
    ExecReload=/bin/kill -s HUP $MAINPID
    #关闭
    ExecStop=/bin/kill -s QUIT $MAINPID
    PrivateTmp=true
    [Install]
    WantedBy=multi-user.target

至此,JDK Tomcat Nginx MySQL全部配置完成。
可以将war包上传到Tomcat的webapp目录下,Tomcat会自动部署。

学习JNDI时的一点感受。

关于JNDI,较为学术向的解释:

1
2
3
4
J2EE 规范要求所有 J2EE 容器都要提供 JNDI 规范的实现。
JNDI 在 J2EE 中的角色就是“交换机” —— J2EE 组件在运行时间接地查找其他组件、资源或服务的通用机制。
在多数情况下,提供 JNDI 供应者的容器可以充当有限的数据存储,这样管理员就可以设置应用程序的执行属性,并让其他应用程序引用这些属性。
在 J2EE 中,JNDI 是把 J2EE 应用程序合在一起的粘合剂,但还没有紧到无法让人很容易地把它们分开并重新装配。

在IBM DeveloperWork我看到了这样一个例子:
https://www.ibm.com/developerworks/cn/java/j-jndi/index.html

大致的内容就是,Dolly在编写JDBC程序时,将数据库驱动、连接池的数量等写死在程序中。
实际部署时,情况发生了变化,但是程序却很难修改。

文章中还提到,J2EE 规范把职责委托给多个开发角色:编写程序的人,打包为程序包的人,部署人员和运维人员。
(实际开发中,1、2的活经常一个人干,3、4则另一个人干, 就是我们熟悉的程序员和运维。)

J2EE中定义了JNDI规范,它的一个最简单的实例,上述例子中也有给出:

1
2
3
4
5
6
<resource-ref>
<description>Dollys DataSource</description>
<res-ref-name>jdbc/mydatasource</res-ref-name>
<res-ref-type>javax.sql.DataSource</res-ref-type>
<res-auth>Container</res-auth>
</resource-ref>

这是一段写在web.xml中的代码,它的作用是构建一个数据源:

1
java:comp/env/jdbc/mydatasource

以便在稍后的编码中使用它。

在查找参考资料时,我见到过一些早期的加载Spring框架的applicationcontext.xml的办法,就是在web.xml中用类似手段定义好一个资源,初始化Spring时,把它作为参数传入进去。

在培训班学习时,我一度认为J2EE是简陋的、原生的、被淘汰的技术。
在我的了解中,现在没有人直接用JSP、Servlet、JDBC开发Web应用,取而代之的是SpringMVC,分层开发,开源框架。
web.xml的作用仅仅是配置拦截器,以及初始化Spring容器,而JDBC和JPA早已不见踪影,EJB也仅仅存在于各种数年前的文档中。

在看完这篇文章后,我发现之前的认识过于浅薄。
J2EE并不是只有Servlet和JSP,EJB实际上是一套类似Spring的IOC/DI容器,它甚至也有AOP的功能。
JNDI接口就是一个用于解耦的规范,将部署人员和运维人员才需要配置的东西独立出去。上文中1和2只需要在程序内定义JNDI格式的数据源,而3和4根据文档配置这个数据源就能使用了。

那么问题来了,为什么Java会有同人逼死官方的现象出现呢……