delete() 和 deleteInBatch()
deleteAll() 和 deleteAllInBatch()
普通删除方法会根据唯一标识字段,生成多条 SQL 语句,而有 InBatch 的方法只会生成一条 SQL 语句
@Transactional
public void save100WEntities() {
for (int i = 0; i < 1000; i++) {
respsitory.save(1000Entities);
entityManager.flush();
entityManager.clear();
}
}
需要满足3个条件:
ResultSet.TYPE_FORWARD_ONLY
ResultSet.CONCUR_READ_ONLY
Integer.MIN_VALUE
Statement statement = connection.createStatement(
ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
statement.setFetchSize(Integer.MIN_VALUE);
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@QueryHints(value = @QueryHint(name = FETCH_SIZE, value = "" + Integer.MIN_VALUE))
Stream<User> findByIdIsNotNull();
}
@Transactional
public void run() {
try (Stream<User> stream = userRepository.findByIdIsNotNull()) {
stream.forEach(user -> {
System.out.println(user);
entityManager.detach(user);
});
}
}
]]>参考 MySQL · 引擎特性 · InnoDB 事务锁系统简介
Locks Set by Different SQL Statements in InnoDB
锁住一段范围,不锁记录本身,通常表示两个索引记录之间,或者索引上的第一条记录之前,或者最后一条记录之后的锁。一般在 RR 隔离级别下会使用。
其实就是锁住 待插入记录本身 + 记录之前的 GAP,Next-Key 锁用于解决 RR 隔离级别下的幻读问题。
通常对于 UPDATE 或 DELETE 或 SELECT … FOR UPDATE 或 SELECT … IN SHARE MODE 操作:
允许多个事务读取,阻止其他事务获取相同数据集的排他锁。
通常 INSERT 操作是不加锁的,但如果在插入或更新记录时,检查到 duplicate key(或者有一个被标记删除的 duplicate key),对于普通的 INSERT/UPDATE,会加 LOCK_S 锁,而对于类似 REPLACE INTO 或者 INSERT … ON DUPLICATE 这样的 SQL 加的是 X 锁。
允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
通常对于 UPDATE 或 DELETE 或类似 SELECT … FOR UPDATE 操作,都会对记录加排他锁
插入意向锁是 GAP 锁的一种,如果有多个 Session 插入同一个 GAP 时,他们无需互相等待,例如当前索引上有记录 4 和 8,两个并发 Session 同时插入记录 6,7。他们会分别为 (4,8) 加上 GAP 锁,但相互之间并不冲突(因为插入的记录不冲突)。
会去检查当前插入位置的下一条记录上是否存在锁对象,这里的下一条记录不是指的物理连续,而是按照逻辑顺序的下一条记录。如果下一条记录上存在锁对象,就需要判断该锁对象是否锁住了 GAP。如果 GAP 被锁住了,并判定和插入意向 GAP 锁冲突,当前操作就需要等待,加的锁类型为 LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION。
对于 GAP 类型且申请的不是插入意向锁时,无需等待任何锁,这是因为不同 Session 对于相同 GAP 可能申请不同类型的锁,而 GAP 锁本身设计为不互相冲突;
任何锁请求都无需等待插入意向锁。
并发插入导致的死锁
create table t1 (a int primary key);
三个会话执行insert into t1(a) values (2);
Session1 | Session2 | Session3 |
---|---|---|
insert,获取 X 锁 | ||
insert,等待获取 S 锁 | ||
insert,等待获取 S 锁 | ||
Rollback,释放 X 锁 | ||
获取 S 锁 | 获得 S 锁 | |
申请插入意向 X 锁,等待 Session 3 | ||
申请插入意向 X 锁,等待 Session 2 |
检测到有 duplicate key 所以加 S 锁。回滚后,同时持有 S 锁,但又都无法获得 插入意向 X 锁。
GAP 锁导致的死锁
create table t1 (a int primary key ,b int);
insert into t1 values (2,2),(6,6),(10,10);
Session1 | Session2 |
---|---|
delete from t where id = 3; 获得 2-5 的 GAP 锁 | |
delete from t where id = 4; 获得 2-5 的 GAP 锁 | |
insert into t values (3, 0); 意向插入锁等待 Session2 释放 GAP 锁 | |
insert into t values (4, 0); 意向插入锁等待 Session2 释放 GAP 锁 |
查看innodb状态(包含最近的死锁日志)
show engine innodb status;
查看事务锁等待状态情况
select * from information_schema.innodb_locks;
select * from information_schema.innodb_lock_waits;
select * from information_schema.innodb_trx;
场景:当 users 表在 insert,update,delete 时,在 users_log 表中记录变更的 id
触发时机:BEFORE,AFTER
触发事件:INSERT,UPDATE,DELETE
NEW 和 OLD 关键字:
| action | NEW | OLD |
|--------|----------------|----------------|
| insert | 插入的新数据 | 无 |
| update | 修改为的新数据 | 被修改的原数据 |
| delete | 无 | 被删除的原数据 |
DELIMITER $
CREATE TRIGGER insert_user AFTER INSERT
ON users FOR EACH ROW
BEGIN
INSERT INTO users_log(user_id, action) VALUES(NEW.id, 'insert');
END$
DELIMITER ;
触发时机:INSTEAD OF(之前),FOR(之后)
触发事件:INSERT,UPDATE,DELETE
INSERTED 和 DELETED 逻辑(概念)表:作用相当于 MySQL 中的 NEW 和 OLD 关键字,不过 MySQL 的代表一条记录,而 SQL Server 的代表所有记录的临时表
CREATE TRIGGER insert_user ON users FOR INSERT AS
BEGIN
DECLARE @id INT
SELECT @id = MIN(id) FROM INSERTED WHILE @id IS NOT NULL
BEGIN
INSERT INTO users_log(user_id, action) VALUES (@id, 'insert')
SELECT @id = MIN(id) FROM INSERTED WHERE id > @id
END
END
触发时机:BEFORE,AFTER
触发事件:INSERT,UPDATE,DELETE
:NEW 和 :OLD 关键字:作用相当于 MySQL 中的 NEW 和 OLD 关键字
CREATE OR REPLACE TRIGGER insert_user AFTER INSERT
ON SCOTT."users" FOR EACH ROW
BEGIN
insert into SCOTT."users_log"("user_id", "action") VALUES (:NEW."id", 'insert');
END;
/
Oracle 不使用 dbms_lock.sleep
实现暂停的方式:
CREATE OR REPLACE PROCEDURE SLEEP (P_MILLI_SECONDS IN NUMBER)
AS LANGUAGE JAVA NAME 'java.lang.Thread.sleep(long)';
BEGIN
INSERT INTO SCOTT.TJJ ("id", "name") VALUES (1, 'a');
SLEEP(10 * 1000);
END;
]]>public interface Animal {
void say();
void run(String addr);
}
public class Dog implements Animal {
@Override
public void say() {
System.out.println("汪汪汪");
}
@Override
public void run(String addr) {
say();
System.out.println("狗在" + addr + "跑");
}
}
public class DogProxy implements Animal {
private Dog dog;
public DogProxy(Dog dog) {
this.dog = dog;
}
@Override
public void say() {
}
@Override
public void run(String addr) {
System.out.println("跑之前");
try {
dog.run(addr);
} finally {
System.out.println("跑之后");
}
}
}
// 调用
new DogProxy(new Dog()).run("路上");
public class InvokeHandler implements InvocationHandler {
private Object object;
public InvokeHandler(Object object) {
this.object = object;
}
/**
* @param proxy 相当于代理类的 this
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用之前");
try {
return method.invoke(object, args);
} finally {
System.out.println("调用之后");
}
}
}
// 调用
InvokeHandler handle = new InvokeHandler(new Dog());
Animal animal = (Animal) Proxy.newProxyInstance(
this.getClass().getClassLoader(), Dog.class.getInterfaces(), handle);
animal.run("路上");
$Proxy
开头快照读 (Snapshot Read):
猜测:在事务中进行 select 时,事务会记录当前正在读取的表的最大 DB_ROW_ID,以后就会读取不大于该 DB_RWO_ID 的记录。
select * from table ...; 不加锁
当前读 (Current Read):
select * from table ... lock in share mode; 加共享锁
select * from table ... for update; 加排它锁
insert ...
update ...
delete ...
快照读读取出来的可能时历史数据,而 update 和 delete 需要获取实时数据。
虽然说事务的隔离级别定义的是读数据的要求,而实际上也可以说是定义了写(当前读)数据的要求。
在 RR 事务级别是存在幻读问题的,虽然快照读可以解决。但是在当前读中,MySQL 是如何解决的?为了解决当前读中的幻读问题,MySQL 使用了 Next-Key 锁。
在使用行锁机制的前提下(当前读),可重复读在读取时对读到的行加锁,这样就可以防止其它事务修改或删除数据;但是无法阻止其它事务新增数据,当事务再次执行相同的 SQL 读取数据时,确发现多出一条,这就是幻读。
虽然说事务的隔离级别定义的是读数据的要求,而实际上也可以说是定义了写(当前读)数据的要求。
Spring way
使用 spring-boot-starter-amqp
会自动配置 ConnectionFactory
、RabbitTemplate
和 AmqpAdmin
三个 Bean。
@Configuration
public class RabbitConfiguration {
@Bean
public DirectExchange exchange() {
return new DirectExchange("infrastructure.direct");
}
@Bean
public Queue queue(){
return new Queue("queueName");
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(exchange()).with("routeKey");
}
}
Just java
ConnectionFactory connectionFactory = new CachingConnectionFactory();
RabbitAdmin amqpAdmin = new RabbitAdmin(connectionFactory);
Queue queue = new Queue("quueuName");
DirectExchange exchange = new DirectExchange("exchangeName");
amqpAdmin.declareQueue(queue);
amqpAdmin.declareExchange(exchange);
amqpAdmin.declareBinding(BindingBuilder.bind(queue).to(exchange).with("routeKey"));
@Autowired
private RabbitTemplate rabbitTemplate;
// 三种方式发送消息
rabbitTemplate.send();
rabbitTemplate.sendAndReceive(); // 用于 RPC 模式的消息发送并等待响应
rabbitTemplate.convertAndSend(); // 将 Java 对象转换成 Message 并发送
在 Listener container 中设置一个简单的 POJO 对象来异步处理消息
消息 Listener 实现
// 三种方式
public class MessageHandler implements MessageListener {
@Override
public void onMessage(Message message) {
}
}
public class MessageHandler implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
}
}
public class MessageHandler {
public void handleMessage(Message message);
}
设置 MessageListenerContainer
@Autowired
private ConnectionFactory connectionFactory;
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("queueName");
container.setMessageListener(new MessageListenerAdapter(new MessageHandler()));
container.setXXX(); 设置队列、listener 等
container.start();
container.addQueueNames(); // 动态添加队列
默认情况下,listener 抛出的所有异常都将包装成 ListenerExecutionFailedException
异常,然后将消息重新放入队列。
因为 MessageListenerContainer
默认设置 ErrorHandler
为 ConditionalRejectingErrorHandler
,ConditionalRejectingErrorHandler
的默认策略是 DefaultExceptionStrategy
,当 DefaultExceptionStrategy
的 isFatal
返回 true 时,该消息将被忽略。
默认发生 MessageConversionException
异常的消息将被丢弃。我们也可以根据情况自己实现 ErrorHandler
来处理消息异常。
新建一个测试配置类 TestApplication
@Configuration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(value = {
Application.class,
ApplicationChecker.class
}, type = FilterType.ASSIGNABLE_TYPE))
public class TestApplication {
// 配置测试使用内存数据库
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
}
新建一个测试公共类 CommonBeanIntegrationTest
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestApplication.class)
// 重新加载 Spring 上下文
// @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public abstract class CommonBeanIntegrationTest {
@After
public void emptyDatabases() {
// 清空数据库,运行速度相对于每次重新加载 Spring 上下文快很多
}
}
正式开始写测试
public class UserTest extends CommonBeanIntegrationTest {
@Autowired
private UserAccessor userAccessor; // 准备测试的 Bean
@Autowired
private UserRepository userRepository; // 测试中需要用到的辅助 Bean
@Before
public void setUp() {
// 每个测试开始前的操作
}
@Test
public void should_save_user_with_username() {
// given
String username = "foo";
// when
userAccessor.save(username);
// then
assertThat(userRepository.findOne(1).getUsername, is(username));
}
@After
public void tearDown() {
// 每个测试执行后的操作
}
}
@RunWith(MockitoJUnitRunner.class)
public class HttpClientTest {
@Mock
private ServerProperties serverProperties;
@InjectMocks // 把 @Mock 注解的对象注入到 httpClient 中
private HttpClient httpClient;
@Test
public void should_get_correct_port(){
when(serverProperties.getPort()).thenReturn(8080);
assertThat(httpClient.get(ApiUrl.list_node).getPort(), is(8080));
}
}
public class UserTest extends CommonBeanIntegrationTest {
@Autowired
private UserRepository userRepository;
@Mock
private HttpClient httpClient;
@Autowired
@InjectMocks
private UserAccessor userAccessor; // 准备测试的 Bean
@Before
public void setUp() {
MockitoAnnotations.initMocks(this); // 初始化并注入 @Mock 注解的对象
// 强制将 Mock 的 httpClient 对象注入 userAccessor 中
// ReflectionTestUtils.setField(userAccessor, "httpClient", httpClient);
}
@Test
public void test_something(){
// Test Code
}
}
同时在需要测试的 Bean 上使用 @Autowired 和 @InjectMocks 注解后,可以达到只 Mock 需要 Mock 的对象,而其它对象依然是注入的真实对象的效果。
但是有时候可能会出现我们 Mock 的对象还是注入的真实对象的情况,此时可以使用 Spring Boot 提供的 ReflectionTestUtils 在 @Before 中强制手动设置需要 Mock 的对象的值。
添加 dependency spring-boot-starter-redis
,它默认使用 jedis
(还支持 JRedis,SRP 和 Lettuce)
一个简单的 Hash 示例:
@SpringBootApplication
public class RedisDemo implements CommandLineRunner {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static void main(String[] args) {
SpringApplication.run(RedisDemo.class).close();
}
public void run(String... args) throws Exception {
BoundHashOperations<String, String, String> ops = stringRedisTemplate.boundHashOps("com.foo");
ops.put("name", "bar");
ops.put("sex", "man");
System.out.println(ops.entries()); // 打印结果:{name=bar, sex=man}
}
}
RedisConnection 负责提供底层的数据操作;而 RedisTemplate 提供对 Redis 数据操作统一的抽象实现,并负责序列化存取的数据以及和 Redis 的连接管理。
StringRedisTemplate 继承自 RedisTemplate,用于操作基于字符串的存取操作。
针对 Redis 不同的存取类型,RedisTemplate 提供了不同的操作视图。
ValueOperations
ListOperations
SetOperations
ZSetOperations
HashOperations
HyperLogLogOperations
spring-data-redis 同时提供了绑定 key 的操作视图,省去每次都需要填写 key 的麻烦。
BoundValueOperations
BoundListOperations
BoundSetOperations
BoundZSetOperations
BoundHashOperations
RedisTemplate 默认采用 JdkSerializationRedisSerializer 序列化,提供的序列化类有:
我们也可以自己实现 RedisSerializer 接口的 serialize 和 deserialize 方法,来实现对自定义对象的序列化和反序列化存取。
@SpringBootApplication
public class RedisDemo implements CommandLineRunner {
@Autowired
private RedisTemplate redisTemplate;
public static void main(String[] args) {
SpringApplication.run(RedisDemo.class).close();
}
public void run(String... args) throws Exception {
redisTemplate.setValueSerializer(new DomainSerializer());
BoundValueOperations<String, Person> ops = redisTemplate.boundValueOps("foo.diy");
ops.getAndSet(new Person("foo"));
System.out.println(ops.get());
}
}
class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Domain{" +
"name='" + name + '\'' +
'}';
}
}
class DomainSerializer implements RedisSerializer<Person> {
@Override
public byte[] serialize(Person person) throws SerializationException {
return person.getName().getBytes();
}
@Override
public Person deserialize(byte[] bytes) throws SerializationException {
return new Person(new String(bytes));
}
}
]]>User user = new User();
user.setUsername("foo");
Email email = new Email();
email.setUser(user); // 必须显示设置 user,否则关联字段值会为空
email.setEmail("foo@bar.com");
user.setEmail(email);
userRepository.save(user);
public class User {
@Id
@GeneratedValue
private Integer id;
@Column
private String username;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "user")
private Email email;
}
public class Email {
@Id
@GeneratedValue
private Integer id;
@OneToOne
private User user; // 默认表字段为:user_id,字段对应规则和一对多一样
@Column
private String email;
}
public class User {
@Id
@GeneratedValue
private Integer id;
@Column
private String username;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "user")
private Email email;
}
public class Email {
@Id
@GeneratedValue
private Integer id;
@OneToOne
@JoinColumn(name = "user_name", referencedColumnName = "username")
private User user; // Email 实体对应的表字段为:user_name
@Column
private String email;
}
]]>public class User {
@Id
@GeneratedValue
private Integer id;
@Column
private String username;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "user_id")
private Set<Email> emails = new HashSet<>();
}
public class Email {
@Id
@GeneratedValue
private Integer id;
/* 默认关联字段名为:驼峰类名转为下划线方式 + "_" + @Id 注解的字段名。
例如 User 的 username 字段有 @Id 注解,则 Email 表关联的字段就默认为 user_username */
@Column(name = "user_id")
private Integer userId;
@Column
private String email;
}
public class User implements Serializable {
@Id
@GeneratedValue
private Integer id;
@Column
private String username;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "user_name", referencedColumnName = "username")
private Set<Email> emails = new HashSet<>();
}
public class Email {
@Id
@GeneratedValue
private Integer id;
@Column(name = "user_name")
private Integer userName;
@Column
private String email;
}
public class User {
@Id
@GeneratedValue
private Integer id;
@Column
private String username;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private Set<Email> emails = new HashSet<>();
}
public class Email {
@Id
@GeneratedValue
private Integer id;
/* 默认关联字段名为:驼峰类名转为下划线方式 + "_" + @Id 注解。的字段名
例如 User 的 username 字段有 @Id 注解,则 Email 表关联的字段就默认为 user_username */
@ManyToOne
private User user; // 默认表字段为:user_id
@Column
private String email;
}
public class User implements Serializable {
@Id
@Column
private String username;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private Set<Email> emails = new HashSet<>();
}
public class Email {
@Id
@GeneratedValue
private Integer id;
@ManyToOne
@JoinColumn(name = "user_name", referencedColumnName = "username")
private User user; // Email 实体对应的表字段为:user_name
@Column
private String email;
}
]]>在 Java 7 新增了 WatchService,它可以用来监控指定目录下的文件改动
import java.io.IOException;
import java.nio.file.*;
public class DirectoryWatcher implements Runnable {
private final Path path; // 监控目录
private final WatchService watchService;
public DirectoryWatcher(Path path) {
this.path = path;
try {
this.watchService = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
throw new RuntimeException("初始化 watchService 出错", e);
}
}
@Override
public void run() {
try {
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
} catch (IOException e) {
throw new RuntimeException("注册监控事件出错", e);
}
while (!Thread.interrupted()) {
if (!watch()) {
break;
}
}
try {
watchService.close();
} catch (IOException e) {
throw new RuntimeException("关闭 watchService 出错", e);
}
}
private boolean watch() {
WatchKey signal;
try {
// 等待监控信号
signal = watchService.take();
} catch (InterruptedException e) {
return false;
}
// 处理监控事件
for (WatchEvent<?> event : signal.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// 处理 OVERFLOW 事件
if (StandardWatchEventKinds.OVERFLOW.equals(kind)) {
continue;
}
System.out.println("事件:" + event.kind() + "," + "文件名:" + event.context());
}
// 重置并继续监控
return signal.reset();
}
public static void main(String[] args) {
new Thread(new DirectoryWatcher(Paths.get("/for/bar"))).run();
}
}
]]>Spring Boot 可以从 properties 文件、YAML 文件、环境变量和命令行参数获取配置。默认 SpringApplication
将从如下位置加载 application.properties
或 application.yml
文件作为配置。
java -jar project.jar --spring.config.location=/path/to/application.yml
/config
subdir of the current directory(application.properties
or application.yml
)application.properties
or application.yml
)/config
package(application.properties
or application.yml
)application.properties
or application.yml
)@PropertySource
或者 @PropertySources
annotations on your @Configuration
classesSpringApplication.setDefaultProperties
)根据配置加载的顺序,前面的配置将会覆盖后面的配置项。常见的配置
使用 @Value("${property}")
注解单个配置项的值
@Value("${user.name}")
private Strin username;
使用 @ConfigurationProperties(prefix="user")
注解类型安全的多个配置项
@Component
@ConfigurationProperties(prefix="user")
public class UserSettings {
private String name;
private int age;
// ... getters and setters
}
子表达式
详见下面 '###子表达式'
字符转义
使用 '\' 来取消元字符的特殊意义,用于匹配元字符本身。
例如:foobar\.com
匹配 'foobar.com'
分支条件
使用 '|' 指定几种规则,匹配其中任意一种
例如:/foo|bar/ 匹配 'foo' 或 /foo|bar/ 匹配 'bar'
贪婪和懒惰
通常正则表达式在使得整个表达式能得到匹配的前提下尽可能多的匹配字符,这被称为贪婪匹配。
有时我们需要尽可能少的匹配字符,可以在限定符后面加上一个 '?',转换为懒惰匹配模式。
例如:'a.*b' 匹配 'aabab',而 'a.*?b' 匹配 'aab'
使用 '()' 建立子表达式(也叫分组),然后可对子表达式进行一些操作。
重复多个字符
例如:(\d{1,3}\.){3}\d{1,3}
匹配 '4.55.666.7'
向后引用,用于重复搜索前面某个分组匹配的文本
例如:'/(\w+),(\1.*)/' 匹配 'foobar,foobar2'
捕获匹配内容
匹配子表达式的文本(也就是分组捕获的内容)可以在表达式或其它程序中作进一步的处理。
默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。分组 0 对应整个正则表达式匹配的内容。
也可以自己指定子表达式的组名,语法为:'(?
不捕获匹配的文本,也不给分组分配组号,语法为 '(?:reg)'。
零宽断言
用于查找在某些内容(但并不包括这些内容)之前或之后的东西。
'(?=exp)' 前置断言,断言自身出现的位置的后面能匹配表达式 exp。
例如:'\w+(?=bar)' 匹配以 bar 结尾的字符串,捕获内容不包含 bar。 'foobar' 捕获的内容为 'foo'
'(?<=exp)' 后置断言,断言自身出现的位置的前面能匹配表达式 exp。
例如:'(?<=foo)\w+' 匹配以 foo 开头的字符串,捕获内容不包含 'foo'。'foobar' 捕获的内容为 'bar'
'(?!exp)' 前置非断言,断言此位置的前面不能匹配表达式 exp。
例如:'/\b(?!foo)\w+\b/' 不匹配以 'foo' 开头的单词
(?<!exp)
后置非断言,断言此位置的后面不能匹配表达式 exp
例如:/\b\w+(?<!bar)\b/
不匹配以 'bar' 结尾的单词
all?
any?
none?
one?
如果有代码块,则以代码块的返回值判断;如果没有代码块,则用自身元素判断。
all?
The method returns true if the block never returns false or nil
any?
The method returns true if the block ever returns a value other than false or nil
none?
The method returns true if the block never returns true for all elements
one?
The method returns true if the block returns true exactly once
[1, 2, 3, 4].all? { |x| x<3 } # false
[1, 2, 3, 4].any? { |x| x<3 } # true
[1, 2, 3, 4].none? { |x| x<1 } # true
[1, 2, 3, 4].one? { |x| x<1 } # false
include?
和 member?
是否包含某个元素
select
和 find_all
选择代码块中为 true 的元素
[1, 2, 3, 4].select { |x| x < 3 } # [1, 2]
reject
选择代码块中为 false 的元素
[1, 2, 3, 4].reject { |x| x < 3 } # [3, 4]
detect
和 find
Returns the first for which block is not false
[1, 2, 3, 4].detect{|x| x >2} # 3
[1, 2, 3, 4].find(Proc.new { 0 }) { |x| x < 1 } # 0
collect
和 map
Returns a new array with the results of running block once for every element in enum.
[1, 2, 3, 4].map(&:to_s) # ["1", "2", "3", "4"]
inject
和 reduce
(5..10).reduce(:+) # 45
(5..10).inject {|sum, n| sum + n } # 45
(5..10).reduce(0, :+) # 45
(5..10).inject{|max, x| max > x ? max : x } # 10
]]>require 'eventmachine'
require 'em-http-server'
class HTTPHandler < EM::HttpServer::Server
# 请求从这里开始执行
def process_http_request
@http # 包含所有 http headers 的 hash
@http_content # http body
# 构造 http response
response = EventMachine::DelegatedHttpResponse.new(self)
response.status = 200
response.content = 'hello, world!'
response.send_response
end
# 请求发生异常时的回调方法
def http_request_errback e
puts "#{e.backtrace.shift}: #{e.message} (#{e.class})"
puts e.backtrace.map{|line| "\tfrom #{line}"}
end
end
EM.run do
# hit Control + C to stop
Signal.trap("INT") { EventMachine.stop }
Signal.trap("TERM") { EventMachine.stop }
EM.start_server 'localhost', 10000, HTTPHandler
end
在
HTTPHandler
中实现http_error_string
方法,返回值就是返回信息。
]]>
EM::HttpServer::Server
<EM::P::HeaderAndContentProtocol
<EM::Connection
全新构建一个 XML
builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
xml.envelope {
xml.header(:identifier => 'IDENTIFIER') {
xml.type('TYPE')
}
xml.content
}
end
在已有 XML 上追加子节点
# 第一种方法
content_element = builder.doc.at('content')
content_element.inner_html = '<source>web</source>'
# 第二种方法
content_element.add_child('<result>SUCCESS</result>')
# 此处调用的是 Nokogiri::XML::Element 的实例方法 <<
content_element << '<reason>REASON</reason>'
# 第三种方法
Nokogiri::XML::Builder.with(builder.doc.root) do |xml|
xml.footer('FOOTER')
xml.destination {
xml.ip('IP')
# 此处调用的是 Nokogiri::XML::Builder 的实例方法 <<
xml << '<port>80</port>'
}
end
修改某个节点的内容
reason_element = builder.doc.at('reason')
reason_element.content = 'MODIFY REASON'
替换某个节点
reason_element.replace('<reason>REPLACE REASON</reason>')
增加或修改某个节点的属性
reason_element['type'] = 'warning'
删除某个节点
reason_element.unlink
content_element.remove
输出 XML
builder.to_xml
假如我们有如下 XML
xml_string = '<?xml version="1.0" encoding="UTF-8"?>
<envelope>
<header identifier="IDENTIFIER">
<type>DOWNLOAD</type>
</header>
<content>
<result>SUCCESS</result>
<files>
<file>a</file>
<file>b</file>
<file>c</file>
</files>
</content>
<destination>
<type>INNER NETWORK</type>
<ip>IP</ip>
<port>80</port>
</destination>
</envelope>'
实例化 XML 对象
doc = Nokogiri::XML(xml_string) { |config|
config.noblanks # 格式化 XML
}
根据 xpath 和 css 方式搜索整个XML节点,返回一个 Nokogiri::XML::NodeSet
对象
doc.xpath('//type')
doc.xpath('//header/type'
doc.css('type')
doc.css('header type')
doc.search('//type')
doc.search('type')
根据 xpath 和 css 方式返回最先找到的第一个 XML 节点,返回一个 Nokogiri::XML::Node
对象
doc.at_xpath('//type')
doc.at_css('type')
doc.at('//type')
doc.at('type')
获取某个节点的属性
header_element = doc.at('header')
header_element.attr('identifier') # 单个属性的值
header_element.attributes # 所有属性的 Hash
header_element.attribute_nodes # 所有属性的 Array
获取某个节点下的子节点
header_element.elements
header_element.element_children
获取某个节点的祖先节点
header_element.ancestors
获取某个节点下的第一个和最后一个子节点,没有则返回 nil
header_element.first_element_child
header_element.last_element_child
输出某个节点下 XML 字符串
header_element.canonicalize # 包含节点自身
header_element.inner_html # 仅输出子节点
Nokogiri::XML::NodeSet
包含多个 Nokogiri::XML::Node
Nokogiri::XML::Element
实例,继承自 Nokogiri::XML::Node
ruby
Nokogiri::XML::Element attributes={'name' => Nokogiri::XML::Attr} children=Nokogiri::XML::NodeSet
Apache Thrift 是 Facebook 实现的一种高效的、支持多种编程语言的远程服务调用的框架。
详情参考官方文档
namespace rb model
struct User {
1: i32 id,
2: string name,
}
service UserStorage {
void set_user(1: User user),
User get_user(1: i32 id),
}
运行 thrift --gen rb user.thrift
,会在 gen-rb
目录下生成 user_constants.rb, user_types.rb, user_storage.rb
三个文件。
# -*- coding: utf-8 -*-
$:.push('gen-rb')
require 'thrift'
require 'user_constants'
require 'user_storage'
class UserHandler
def set_user(user)
puts 'Set user: ' + user.inspect
end
def get_user(id)
return Model::User.new(:id => id, :name => "Name: #{id}")
end
end
processor = Model::UserStorage::Processor.new(UserHandler.new())
transport = Thrift::ServerSocket.new(9090)
transportFactory = Thrift::BufferedTransportFactory.new()
server = Thrift::SimpleServer.new(processor, transport, transportFactory)
puts "Starting the server..."
server.serve()
puts "Stoped the server..."
实现对应的服务操作,并打开 9090 端口等待客户端调用
# -*- coding: utf-8 -*-
$:.push('gen-rb')
require 'thrift'
require 'user_constants'
require 'user_storage'
begin
transport = Thrift::BufferedTransport.new(Thrift::Socket.new('0.0.0.0', '9090'))
protocol = Thrift::BinaryProtocol.new(transport)
client = Model::UserStorage::Client.new(protocol)
transport.open()
user = Model::User.new(:id => 1, :name => 'foo')
puts client.set_user(user).inspect # => nil
puts client.get_user(2).inspect # => <Model::User id:2, name:"Name: 2">
transport.close()
rescue Thrift::TransportException => e
puts "Thrift::Exception: #{e.message}"
end
]]>