You have been logged out.
-
-
Sign In
-
用户名或密码错误
-
+
You have been logged out.
+
+
+
+ 用户名可以随意填写,密码都是password,密码填写其他字符会出现用户名或密码错的提示
+ 只要登录的用户都会被赋予 "USER" 的角色,如果登录名是admin,则还会被赋予 "ADMIN" 的角色
+ / 不登录也可访问
+ /user "USER" 角色即可访问
+ /admin 只有 "ADMIN"角色才可访问
+ 上述的访问限制是在WebSecurityConfig中设定的,也可通过注解在Controller里设置,参见section-07,此节不再举例
+
+
\ No newline at end of file
diff --git a/section-12-mvc-security/src/main/resources/templates/user.html b/section-12-mvc-security/src/main/resources/templates/user.html
new file mode 100644
index 0000000..4e17a66
--- /dev/null
+++ b/section-12-mvc-security/src/main/resources/templates/user.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
USER
+
+
+
USER
+
+
+
+
+
+
+
此段内容具有USER角色的用户可见。
+
此段内容具有USER角色的用户可见。
+
+
此段内容具有edit权限的用户可见。
+
此段内容具有delete权限的用户可见。
+
+
+ 此段内容所有登录用户均可见
+
+
+
+
+
+
diff --git a/section-14-dynamic-datasource/README.md b/section-14-dynamic-datasource/README.md
new file mode 100644
index 0000000..c967cc0
--- /dev/null
+++ b/section-14-dynamic-datasource/README.md
@@ -0,0 +1,55 @@
+动态数据源
+===========================
+
+这是一个并**不常见**的需求的实现方案,记录在此为了以后再遇到时查找方便。
+
+## 需求
+
+整个项目中有一个主数据库(肯定存在)和若干可以在主库的某个表内配置的数据库组成。例如:每个客户有一个单独的数据库,客户数据存在客户数据库内。
+
+主数据库:main_db
+
+若干客户数据库,在main_db.customer内记录
+
+每个请求默认使用客户的数据库,请求会在request.header中增加一个customerId来标志是哪个客户。
+
+## 数据库
+
+main_db内有一个表
+
+```sql
+create table customer(
+ id serial primary key,
+
+ dataSourceClassName varchar(100),
+ dataSourceUser varchar(50),
+ dataSourcePassword varchar(50),
+ dataSourceDataBaseName varchar(50),
+ dataSourcePortNumber int,
+ dataSourceServerName varchar(50)
+)
+
+insert into customer(dataSourceClassName, dataSourceUser, dataSourcePassword, dataSourceDataBaseName, dataSourcePortNumber, dataSourceServerName)
+ values('org.postgresql.ds.PGSimpleDataSource', 'pgdbo', '', 'tvseries', 5432, '127.0.0.1');
+
+```
+
+上表中登记的tvseries数据库,和其他章节的数据库相同。
+
+## 代码说明
+
+* [DataSourceConfig.java](src/main/java/cn/devmgr/tutorial/DataSourceConfig.java) 实现动态数据源的配置
+* [DbFilter.java](src/main/java/cn/devmgr/tutorial/DbFilter.java) 对每个request,获取request header(CustomerId)并记录到ThreadLocal,以便DynamicDataSource根据ThreadLocal内值选择数据源
+* [ThreadLocalContext.java](src/main/java/cn/devmgr/tutorial/ThreadLocalContext.java) 用于记录ThreadLocal中变量的统一处理类
+* [CustomerService.java](src/main/java/cn/devmgr/tutorial/service/CustomerService.java) 在程序内动态切换数据源的例子
+
+
+## 测试
+
+使用浏览器直接访问,因为没有传递request header customerId会出现500错误。
+
+使用curl,传递了customerId的header,且值在customer表内存在则不会出错了。
+
+```bash
+curl -H "customerId:1" http://localhost:8080/tvseries/
+```
diff --git a/section-14-dynamic-datasource/pom.xml b/section-14-dynamic-datasource/pom.xml
new file mode 100644
index 0000000..00cf74c
--- /dev/null
+++ b/section-14-dynamic-datasource/pom.xml
@@ -0,0 +1,82 @@
+
+
+ 4.0.0
+
+ cn.devmgr.tutorial
+ tutorial-section14-dynamic-datasource
+ 0.0.1-SNAPSHOT
+ jar
+
+ tutorial-02
+ 用Spring Boot编写RESTful API教程 02
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.0.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 1.3.2
+
+
+ org.postgresql
+ postgresql
+
+
+
+ commons-io
+ commons-io
+ 2.7
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.13
+
+
+
+ org.apache.httpcomponents
+ httpmime
+ 4.5.3
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/section-14-dynamic-datasource/sql.sql b/section-14-dynamic-datasource/sql.sql
new file mode 100644
index 0000000..d4d27bf
--- /dev/null
+++ b/section-14-dynamic-datasource/sql.sql
@@ -0,0 +1,36 @@
+create database main_db;
+
+create table customer(
+ id serial primary key,
+
+ dataSourceClassName varchar(100),
+ dataSourceUser varchar(50),
+ dataSourcePassword varchar(50),
+ dataSourceDataBaseName varchar(50),
+ dataSourcePortNumber int,
+ dataSourceServerName varchar(50)
+);
+
+insert into customer(dataSourceClassName, dataSourceUser, dataSourcePassword, dataSourceDataBaseName, dataSourcePortNumber, dataSourceServerName)
+ values('org.postgresql.ds.PGSimpleDataSource', 'pgdbo', '', 'tvseries', 5432, '127.0.0.1');
+
+
+
+create database tvseries;
+
+/** TABLES **/
+create table tv_series(
+ id serial primary key,
+ name varchar(50) not null,
+ season_count int not null,
+ origin_release date not null,
+ status smallint not null default 0,
+ delete_reason varchar(100) null
+);
+
+create table tv_character(
+ id serial primary key,
+ tv_series_id int not null,
+ name varchar(50) not null,
+ photo varchar(100) null
+);
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/Application.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/Application.java
new file mode 100644
index 0000000..8922063
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/Application.java
@@ -0,0 +1,17 @@
+package cn.devmgr.tutorial;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+
+@SpringBootApplication
+@MapperScan("cn.devmgr.tutorial.dao")
+@EnableConfigurationProperties
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/DataSourceConfig.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/DataSourceConfig.java
new file mode 100644
index 0000000..a7c3a75
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/DataSourceConfig.java
@@ -0,0 +1,151 @@
+package cn.devmgr.tutorial;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+
+/**
+ * 此配置实现了这种需求: 系统内有一个主数据库,配置在application.yml中turtorial.datasource下;
+ * 此数据源内有一张表,表内记载若干数据库的信息(dataSourceClassName, serverName, port, dbName ...等)
+ * 每个请求根据request.getHeader("customerId")的不同,而动态选择表内的一个数据源。
+ * 每个请求会在DbFilter中设置ThreadLocalContext来标记当前的customerId。
+ * 在此类提供的DynamicDataSource中根据ThreadLocalContext中的值来决定使用哪个数据源
+ *
+ * 如果在一个请求中,需要使用不同的数据源,在service中,调用其他数据源前,可以用用ThreadLocalContent.setDbKey()来切换
+ */
+@Configuration
+public class DataSourceConfig {
+ private static final Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
+
+ @Value("${tutorial.datasource.className}") String dataSourceClassName;
+ @Value("${tutorial.datasource.serverName}") String dataSourceServerName;
+ @Value("${tutorial.datasource.portNumber}") int dataSourcePortNumber;
+ @Value("${tutorial.datasource.dataBaseName}") String dataSourceDataBaseName;
+ @Value("${tutorial.datasource.user}") String dataSourceUser;
+ @Value("${tutorial.datasource.password}") String dataSourcePassword;
+
+
+ public class DynamicDataSource extends AbstractRoutingDataSource {
+
+ @Override
+ protected Object determineCurrentLookupKey() {
+ String key = ThreadLocalContext.getDbKey();
+ DataSourceConfig.this.logger.trace("determineCurrentLookupKey...{}", key);
+ if(key == null){
+ RuntimeException re = new RuntimeException("无法确定使用哪个DataSource,在ThreadLocalContext未找到设置");
+ logger.error(re);
+ throw re;
+ }
+ return key;
+ }
+
+ }
+
+
+ @Bean
+ @Primary
+ public SqlSessionFactoryBean sqlSessionFactory() {
+ logger.trace("sqlSessionFactory()");
+ DataSource ds = dynamicDataSource();
+ SqlSessionFactoryBean sfb = new SqlSessionFactoryBean();
+ sfb.setDataSource(ds);
+ return sfb;
+ }
+
+ @Bean
+ public DynamicDataSource dynamicDataSource() {
+ logger.trace("DynamicDataSource()....");
+ try {
+ DynamicDataSource dds = new DynamicDataSource();
+ dds.setTargetDataSources(createTargetDataSources());
+ return dds;
+ }catch(Exception e){
+ logger.error("创建动态数据源失败", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ private DataSource createMainDataSource(){
+ logger.trace("mainDataSource: {}, {}, {}, {}, {}, {}", dataSourceClassName, dataSourceServerName,
+ dataSourcePortNumber, dataSourceDataBaseName, dataSourceUser, dataSourcePassword);
+ DataSource main = createDataSource(dataSourceClassName, dataSourceServerName, dataSourcePortNumber,
+ dataSourceDataBaseName, dataSourceUser, dataSourcePassword);
+ return main;
+ }
+
+ private Map
createTargetDataSources() throws SQLException {
+ logger.trace("createTargetDataSources");
+ Map map = new HashMap<>();
+
+ //把所有的数据源都放入到map
+ DataSource main = createMainDataSource();
+ map.put("main", main);
+
+ Connection conn = main.getConnection();
+ Statement stat = conn.createStatement();
+ ResultSet rs = stat.executeQuery("select id, dataSourceClassName, dataSourceUser, dataSourcePassword, " +
+ "dataSourceDataBaseName, dataSourcePortNumber, dataSourceServerName from customer ");
+ while(rs.next()){
+ String id = rs.getString("id");
+ String className = rs.getString("dataSourceClassName");
+ String serverName = rs.getString("dataSourceServerName");
+ int port = rs.getInt("dataSourcePortNumber");
+ String dbName = rs.getString("dataSourceDataBaseName");
+ String user = rs.getString("dataSourceUser");
+ String password = rs.getString("dataSourcePassword");
+ logger.trace("adding datasource {} {}", id, dbName);
+ map.put(id, createDataSource(className, serverName, port, dbName, user, password));
+ }
+ rs.close();
+ stat.close();
+ conn.close();
+ return map;
+ }
+
+ private DataSource createDataSource(String className, String serverName, int port, String dbName,
+ String user, String password) {
+ logger.trace("createDataSource({}, {}, {}, {}, {}, {})", className, serverName, port, dbName, user, password);
+ Properties props = new Properties();
+ props.setProperty("dataSourceClassName", className);
+ props.setProperty("dataSource.serverName", serverName);
+ if(port > 0) {
+ props.setProperty("dataSource.portNumber", String.valueOf(port));
+ }
+ props.setProperty("dataSource.databaseName", dbName);
+ if(user != null) {
+ props.setProperty("dataSource.user", user);
+ }
+ if(password != null) {
+ props.setProperty("dataSource.password", password);
+ }
+ props.put("dataSource.logWriter", new PrintWriter(System.out));
+
+ HikariConfig config = new HikariConfig(props);
+ config.setMaximumPoolSize(15); //连接池最多15个连接
+
+ HikariDataSource ds = new HikariDataSource(config);
+
+ return ds;
+ }
+
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/DbFilter.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/DbFilter.java
new file mode 100644
index 0000000..02960b7
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/DbFilter.java
@@ -0,0 +1,30 @@
+package cn.devmgr.tutorial;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class DbFilter extends OncePerRequestFilter {
+ private static final Logger logger = LoggerFactory.getLogger(DbFilter.class);
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
+ String customerId = request.getHeader("customerId");
+ if(customerId == null){
+ logger.warn("请求没有传递customerId参数");
+ }else{
+ logger.trace("本次请求customerId: {}", customerId);
+ ThreadLocalContext.setCustomerId(customerId);
+ }
+
+ chain.doFilter(request, response);
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/KeyValueGroup.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/KeyValueGroup.java
new file mode 100644
index 0000000..e4097b9
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/KeyValueGroup.java
@@ -0,0 +1,43 @@
+package cn.devmgr.tutorial;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+@ConfigurationProperties(prefix="tutorial", ignoreInvalidFields = true)
+public class KeyValueGroup {
+
+ private Map> maps;
+
+ public Map> getMaps() {
+ return maps;
+ }
+
+ public void setMaps(Map> maps) {
+ this.maps = maps;
+ }
+
+ // private Map types;
+
+// public Map getCategories() {
+// return categories;
+// }
+//
+// public void setCategories(Map categories) {
+// this.categories = categories;
+// }
+//
+// public Map getTypes() {
+// return types;
+// }
+//
+// public void setTypes(Map types) {
+// this.types = types;
+// }
+
+ public String toString(){
+ return this.getClass().getName() + "<" + this.maps + "; " + ">";
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/ThreadLocalContext.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/ThreadLocalContext.java
new file mode 100644
index 0000000..8c7402f
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/ThreadLocalContext.java
@@ -0,0 +1,113 @@
+package cn.devmgr.tutorial;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ThreadLocalContext {
+ private static final Logger logger = LoggerFactory.getLogger(ThreadLocalContext.class);
+
+ public static final String CUSTOMERID = "CUSTOMER_ID";
+ public static final String DBKEY = "DB_KEY";
+
+
+ private ThreadBindings threadBindings = new ThreadBindings();
+
+ private static ThreadLocalContext instance = new ThreadLocalContext();
+
+ protected class ThreadBindings extends ThreadLocal> {
+ private String instanceTime = (new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")).format(new Date());
+
+ public Map initialValue() {
+ return new HashMap();
+ }
+
+ public Map getBindings() {
+ return (Map) get();
+ }
+
+ @Override
+ public String toString(){
+ return super.toString() + " instanced at " + instanceTime + "; keys=" + getBindings().keySet().toString();
+ }
+
+ }
+
+ private ThreadLocalContext(){
+ }
+
+ public void set(String name, Object value) {
+ Map bindings = threadBindings.getBindings();
+ if (bindings == null) {
+ logger.warn("setInThread: no bindings!");
+ return;
+ }
+
+ if (value == null) {
+ bindings.remove(name);
+ }else {
+ bindings.put(name, value);
+ }
+ }
+
+ public void clear(){
+ Map, ?> bindings = threadBindings.getBindings();
+ if (bindings == null) {
+ if(logger.isInfoEnabled()){
+ logger.info("clear: no bindings!");
+ }
+ return;
+ }
+ bindings.clear();
+ threadBindings.remove();
+ }
+
+
+ public Object get(String name) {
+ Map, ?> bindings = threadBindings.getBindings();
+ if (bindings == null) {
+ logger.info("get: no bindings!");
+ return null;
+ }
+
+ return bindings.get(name);
+ }
+
+
+ public static ThreadLocalContext getInstance(){
+ return instance;
+ }
+
+ public static void setValue(String name, Object value){
+ instance.set(name, value);
+ }
+
+ public static Object getValue(String name){
+ return instance.get(name);
+ }
+
+ public static void setCustomerId(String value){
+ setValue(CUSTOMERID, value);
+ }
+ public static String getCustomerId(){
+ return (String) getValue(CUSTOMERID);
+ }
+
+ public static void setDbKey(String value){
+ setValue(DBKEY, value);
+ }
+
+
+ public static String getDbKey(){
+ String key = (String) getValue(DBKEY);
+ //设置的DbKey优先;如果未设置,使用customerId
+ if(key == null){
+ key = getCustomerId();
+ }
+ return key;
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/controller/TvSeriesController.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/controller/TvSeriesController.java
new file mode 100644
index 0000000..14c5054
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/controller/TvSeriesController.java
@@ -0,0 +1,110 @@
+package cn.devmgr.tutorial.controller;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import cn.devmgr.tutorial.KeyValueGroup;
+import cn.devmgr.tutorial.service.CustomerService;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import cn.devmgr.tutorial.pojo.TvSeries;
+import cn.devmgr.tutorial.service.TvSeriesService;
+
+/**
+ * RestController的一个例子,展示了各种基本操作在RestController的实现方式。
+ *
+ */
+@RestController
+@RequestMapping("/tvseries")
+public class TvSeriesController {
+ private final Log log = LogFactory.getLog(TvSeriesController.class);
+
+ @Value("${tutorial.uploadFolder:target/files}") String uploadFolder;
+ @Autowired TvSeriesService tvSeriesService;
+
+ @Autowired
+ CustomerService customerService;
+
+ @Autowired
+ KeyValueGroup kvg;
+
+ @GetMapping
+ public Map getAll() {
+ if(log.isTraceEnabled()) {
+ log.trace("getAll() ");
+ }
+ log.trace(kvg);
+ log.trace(kvg.getMaps().get("categories").get(1));
+
+ Map result = new HashMap<>();
+
+ List list = tvSeriesService.getAllTvSeries();
+ result.put("list", list);
+ result.put("customer", customerService.getCustomerById(1));
+ result.put("tvseries", tvSeriesService.getTvSeriesById(1));
+ return result;
+ }
+
+ /**
+ * 给电视剧添加剧照。
+ * 这是一个文件上传的例子(上传代码参照test.UploadFile.main)
+ */
+ @PostMapping(value="/{tvSeriesId}/photos", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
+ @ResponseBody
+ public Map addPhoto(@PathVariable String tvSeriesId, @RequestParam String fileId, @RequestParam("photo") MultipartFile[] imgFiles) throws Exception{
+ if(log.isTraceEnabled()) {
+ log.trace("接受到 " + tvSeriesId + "收到文件:" + imgFiles == null ? 0 : imgFiles.length); //.getOriginalFilename());
+ }
+ //保存文件
+ File folder = new File(uploadFolder);
+ if(!folder.exists()) {
+ folder.mkdirs();
+ }
+ if(imgFiles == null){
+ return null;
+ }
+ Map result = new HashMap<>();
+ List files = new ArrayList<>();
+ for(MultipartFile imgFile : imgFiles) {
+ String fileName = imgFile.getOriginalFilename();
+ if (fileName.endsWith(".jpg")) {
+ FileOutputStream fos = new FileOutputStream(new File(folder, fileName));
+ IOUtils.copy(imgFile.getInputStream(), fos);
+ fos.close();
+ files.add(fileName);
+ } else {
+ throw new RuntimeException("不支持的格式,仅支持jpg格式");
+ }
+ }
+ result.put("photos", files);
+ result.put("tvSeriesId", tvSeriesId);
+ result.put("fileId", fileId);
+ return result;
+ }
+
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/CustomerDao.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/CustomerDao.java
new file mode 100644
index 0000000..93d1b90
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/CustomerDao.java
@@ -0,0 +1,13 @@
+package cn.devmgr.tutorial.dao;
+
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import java.util.Map;
+
+@Repository
+public interface CustomerDao {
+
+ @Select("select * from customer where id=#{id}")
+ public Map getCustomerById(int id);
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/TvCharacterDao.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/TvCharacterDao.java
new file mode 100644
index 0000000..096ddda
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/TvCharacterDao.java
@@ -0,0 +1,25 @@
+package cn.devmgr.tutorial.dao;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import cn.devmgr.tutorial.pojo.TvCharacter;
+
+@Repository
+public interface TvCharacterDao {
+
+ @Select("select * from tv_character where id=#{id}")
+ public TvCharacter getOneById(int id);
+
+ @Select("select * from tv_character where tv_series_id=#{tvSeriesId}")
+ public List getAllByTvSeriesId(int tvSeriesId);
+
+ public int update(TvCharacter tvCharacter);
+ public int insert(TvCharacter tvCharacter);
+
+ @Delete("delete from tv_character where id=#{id}")
+ public int delete(int id);
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/TvSeriesDao.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/TvSeriesDao.java
new file mode 100644
index 0000000..5447f00
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/dao/TvSeriesDao.java
@@ -0,0 +1,27 @@
+package cn.devmgr.tutorial.dao;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import cn.devmgr.tutorial.pojo.TvSeries;
+
+public interface TvSeriesDao {
+
+ @Select("select * from tv_series where id=#{id}")
+ public TvSeries getOneById(int id);
+
+ @Select("select * from tv_series where status=0")
+ public List getAll();
+
+ public int update(TvSeries tvSeries);
+ public int insert(TvSeries tvSeries);
+
+ @Delete("delete from tv_series where id=#{id}")
+ public int delete(int id);
+
+ @Update("update tv_series set status=-1, reason=#{reason} where id=#{id}")
+ public int logicDelete(int id, String reason);
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/pojo/TvCharacter.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/pojo/TvCharacter.java
new file mode 100644
index 0000000..3db488f
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/pojo/TvCharacter.java
@@ -0,0 +1,48 @@
+package cn.devmgr.tutorial.pojo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * 电视剧中的人物
+ *
+ */
+public class TvCharacter {
+ private Integer id;
+ private int tvSeriesId;
+ private String name;
+ @JsonIgnore private String photo;
+
+ public Integer getId() {
+ return id;
+ }
+ public void setId(Integer id) {
+ this.id = id;
+ }
+ public int getTvSeriesId() {
+ return tvSeriesId;
+ }
+ public void setTvSeriesId(int tvSeriesId) {
+ this.tvSeriesId = tvSeriesId;
+ }
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPhoto() {
+ return photo;
+ }
+ public void setPhoto(String photo) {
+ this.photo = photo;
+ }
+
+ public String getPhotoUrl() {
+ if(this.photo == null) {
+ return null;
+ }else {
+ return "http://127.0.0.1/photos/" + this.photo;
+ }
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/pojo/TvSeries.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/pojo/TvSeries.java
new file mode 100644
index 0000000..5a3c454
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/pojo/TvSeries.java
@@ -0,0 +1,57 @@
+package cn.devmgr.tutorial.pojo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 电视剧
+ *
+ */
+public class TvSeries {
+ private Integer id;
+ private String name;
+ private int seasonCount;
+ private Date originRelease;
+ private List tvCharacters;
+
+
+ public Integer getId() {
+ return id;
+ }
+ public void setId(Integer id) {
+ this.id = id;
+ }
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+ public int getSeasonCount() {
+ return seasonCount;
+ }
+ public void setSeasonCount(int seasonCount) {
+ this.seasonCount = seasonCount;
+ }
+ public Date getOriginRelease() {
+ return originRelease;
+ }
+ public void setOriginRelease(Date originRelease) {
+ this.originRelease = originRelease;
+ }
+ public List getTvCharacters() {
+ return tvCharacters;
+ }
+ public void setTvCharacters(List tvCharacters) {
+ this.tvCharacters = tvCharacters;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(this.getClass().getName()).append("{id:").append(id);
+ buf.append(",name:").append(name).append(",seasonCount:").append(seasonCount);
+ buf.append("}");
+ return buf.toString();
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/service/CustomerService.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/service/CustomerService.java
new file mode 100644
index 0000000..ba50e92
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/service/CustomerService.java
@@ -0,0 +1,28 @@
+package cn.devmgr.tutorial.service;
+
+import cn.devmgr.tutorial.ThreadLocalContext;
+import cn.devmgr.tutorial.dao.CustomerDao;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Map;
+
+@Service
+public class CustomerService {
+
+ @Autowired
+ CustomerDao customerDao;
+
+ /**
+ * 使用主数据库查询的例子;
+ * @param id
+ * @return
+ */
+ public Map getCustomerById(int id){
+ ThreadLocalContext.setDbKey("main"); //切换到主数据库
+ Map map = customerDao.getCustomerById(id);
+ ThreadLocalContext.setDbKey(null); //切换回原有数据库
+ return map;
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/service/TvSeriesService.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/service/TvSeriesService.java
new file mode 100644
index 0000000..09c7efd
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/service/TvSeriesService.java
@@ -0,0 +1,91 @@
+package cn.devmgr.tutorial.service;
+
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import cn.devmgr.tutorial.dao.TvCharacterDao;
+import cn.devmgr.tutorial.dao.TvSeriesDao;
+import cn.devmgr.tutorial.pojo.TvCharacter;
+import cn.devmgr.tutorial.pojo.TvSeries;
+
+@Service
+public class TvSeriesService {
+ private final Log log = LogFactory.getLog(TvSeriesService.class);
+
+ @Autowired private TvSeriesDao seriesDao;
+ @Autowired private TvCharacterDao characterDao;
+
+ @Transactional(readOnly=true)
+ public List getAllTvSeries(){
+ try {
+ Thread.sleep(10);
+ }catch(Exception e) {
+
+ }
+ if(log.isTraceEnabled()) {
+ log.trace("getAllTvSeries started ");
+ }
+ List list = seriesDao.getAll();
+
+ return list;
+ }
+
+ @Transactional(readOnly=true)
+ public TvSeries getTvSeriesById(int tvSeriesId) {
+ if(log.isTraceEnabled()) {
+ log.trace("getTvSeriesById started for " + tvSeriesId);
+ }
+ TvSeries series = seriesDao.getOneById(tvSeriesId);
+ if(series != null) {
+ series.setTvCharacters(characterDao.getAllByTvSeriesId(tvSeriesId));
+ }
+ return series;
+ }
+
+ public TvSeries updateTvSeries(TvSeries tvSeries) {
+ if(log.isTraceEnabled()) {
+ log.trace("updateTvSeries started for " + tvSeries);
+ }
+ seriesDao.update(tvSeries);
+ return tvSeries;
+ }
+
+ @Transactional(propagation=Propagation.REQUIRES_NEW)
+ public TvSeries addTvSeries(TvSeries tvSeries) {
+ if(log.isTraceEnabled()) {
+ log.trace("addTvSeries started for " + tvSeries);
+ }
+ seriesDao.insert(tvSeries);
+ if(tvSeries.getId() == null) {
+ throw new RuntimeException("cannot got primary key id");
+ }
+ if(tvSeries.getTvCharacters() != null) {
+ for(TvCharacter tc : tvSeries.getTvCharacters()) {
+ tc.setTvSeriesId(tvSeries.getId().intValue());
+ characterDao.insert(tc);
+ }
+ }
+ return tvSeries;
+ }
+
+ public TvCharacter updateTvCharacter(TvCharacter tvCharacter) {
+ characterDao.update(tvCharacter);
+ return tvCharacter;
+ }
+
+ public TvCharacter addTvCharacter(TvCharacter tvCharacter) {
+ characterDao.insert(tvCharacter);
+ return tvCharacter;
+ }
+
+ public boolean deleteTvSeries(int id, String reason) {
+ seriesDao.logicDelete(id, reason);
+ return true;
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/test/UploadFile.java b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/test/UploadFile.java
new file mode 100644
index 0000000..db97a2d
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/java/cn/devmgr/tutorial/test/UploadFile.java
@@ -0,0 +1,60 @@
+package cn.devmgr.tutorial.test;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * 用HttpClient调用RESTful API (TvSeriesController.addPhoto)
+ * 实现文件上传的例子
+ */
+public class UploadFile {
+
+ public static void main(String[] argvs) throws Exception{
+ File f1 = new File("src/test/resources/1.jpg");
+ File f2 = new File("src/test/resources/2.jpg");
+
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+
+ HttpEntity data = MultipartEntityBuilder.create()
+ .setMode(HttpMultipartMode.RFC6532)
+ .addBinaryBody("photo", f1, ContentType.DEFAULT_BINARY, f1.getName())
+ .addBinaryBody("photo", f2, ContentType.DEFAULT_BINARY, f2.getName())
+ .addTextBody("fileId", "110", ContentType.DEFAULT_BINARY)
+ .build();
+
+ HttpUriRequest request = RequestBuilder
+ .post("http://127.0.0.1:8080/tvseries/10/photos")
+ .setEntity(data)
+ .build();
+
+ ResponseHandler responseHandler = new ResponseHandler() {
+ @Override
+ public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
+ int status = response.getStatusLine().getStatusCode();
+ System.out.println(status);
+ if (status == 200) {
+ HttpEntity entity = response.getEntity();
+ return entity != null ? EntityUtils.toString(entity) : null;
+ } else {
+ throw new ClientProtocolException("Unexpected response status: " + status);
+ }
+ }
+ };
+ String responseBody = httpClient.execute(request, responseHandler);
+ System.out.println(responseBody);
+ }
+ }
+}
diff --git a/section-14-dynamic-datasource/src/main/resources/application.yml b/section-14-dynamic-datasource/src/main/resources/application.yml
new file mode 100644
index 0000000..354aaf2
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/resources/application.yml
@@ -0,0 +1,45 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: spring-boot-tutorial
+# datasource:
+# driverClassName: org.postgresql.Driver
+# url: jdbc:postgresql://127.0.0.1:5432/tvseries?stringtype=unspecified&ApplicationName=tutorial
+# username: pgdbo
+# password: sql
+ jackson:
+ serialization:
+ write-dates-as-timestamps: true #使用数值timestamp表示日期
+
+mybatis.configuration.mapUnderscoreToCamelCase: true
+
+logging:
+ file: target/app.log
+ level:
+ ROOT: WARN
+ cn.devmgr: TRACE
+ cn.devmgr.tutorial.dao: WARN
+
+#此项目的自定义属性
+tutorial:
+ uploadFolder: target/upload-files/ #文件上传目录,可以用/开头的绝对路径
+ datasource:
+ className: org.postgresql.ds.PGSimpleDataSource #使用HikariDataSource的配置
+ serverName: 127.0.0.1
+ portNumber: 5432
+ dataBaseName: main_db
+ user: pgdbo
+ password: sql
+ maps:
+ aaa: bbb
+ categories:
+ 1: 类别A
+ 2: 类别B
+ 3: 类别3
+ types:
+ 1: 甲
+ 2: 乙
+ 3: 丙
+ 4: 丁
diff --git a/section-14-dynamic-datasource/src/main/resources/cn/devmgr/tutorial/dao/TvCharacterDao.xml b/section-14-dynamic-datasource/src/main/resources/cn/devmgr/tutorial/dao/TvCharacterDao.xml
new file mode 100644
index 0000000..f16b4c6
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/resources/cn/devmgr/tutorial/dao/TvCharacterDao.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ insert into tv_character (name, tv_series_id, photo )
+ values (#{name}, #{tvSeriesId}, #{photo} )
+
+
+
+ update tv_character set name=#{name}, tv_series_id=#{tvseriesid}, photo=#{photo} where id=#{id}
+
+
+
\ No newline at end of file
diff --git a/section-14-dynamic-datasource/src/main/resources/cn/devmgr/tutorial/dao/TvSeriesDao.xml b/section-14-dynamic-datasource/src/main/resources/cn/devmgr/tutorial/dao/TvSeriesDao.xml
new file mode 100644
index 0000000..90a94a2
--- /dev/null
+++ b/section-14-dynamic-datasource/src/main/resources/cn/devmgr/tutorial/dao/TvSeriesDao.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ insert into tv_series (name, season_count, origin_release )
+ values (#{name}, #{seasonCount}, #{originRelease} )
+
+
+
+ update tv_series set name=#{name}, season_count=#{seasonCount}, origin_release=#{originRelease} where id=#{id}
+
+
+
\ No newline at end of file
diff --git a/section-14-dynamic-datasource/src/test/java/cn/devmgr/tutorial/AppTests.java b/section-14-dynamic-datasource/src/test/java/cn/devmgr/tutorial/AppTests.java
new file mode 100644
index 0000000..6a11ece
--- /dev/null
+++ b/section-14-dynamic-datasource/src/test/java/cn/devmgr/tutorial/AppTests.java
@@ -0,0 +1,16 @@
+package cn.devmgr.tutorial;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class AppTests {
+
+ @Test
+ public void contextLoads() throws Exception {
+ }
+
+}
diff --git a/section-14-dynamic-datasource/src/test/resources/1.jpg b/section-14-dynamic-datasource/src/test/resources/1.jpg
new file mode 100644
index 0000000..cffb47d
Binary files /dev/null and b/section-14-dynamic-datasource/src/test/resources/1.jpg differ
diff --git a/section-14-dynamic-datasource/src/test/resources/2.jpg b/section-14-dynamic-datasource/src/test/resources/2.jpg
new file mode 100644
index 0000000..d042225
Binary files /dev/null and b/section-14-dynamic-datasource/src/test/resources/2.jpg differ
diff --git a/section-15-dynamic-sql/README.md b/section-15-dynamic-sql/README.md
new file mode 100644
index 0000000..cbb0461
--- /dev/null
+++ b/section-15-dynamic-sql/README.md
@@ -0,0 +1,17 @@
+Mybatis Dynamic SQL
+===========================
+
+
+## 导出Excel的例子
+
+[程序说明请看这里](https://github.com/gexiangdong/tutorial/wiki/%E5%AF%BC%E5%87%BAexcel)
+
+[SampleController](./src/main/java/cn/devmgr/tutorial/controller/SampleController.java) 内有一个导出Excel格式数据的例子。
+
+
+## 自定义的Mybatis Language Driver
+
+[DynamicTableLanguageDriver](./src/main/java/cn/devmgr/tutorial/DynamicTableLanguageDriver.java) 是一个自定义的Mybatis Language Driver,它可以根据需要动态生成SQL。此例是根据传入的参数,自动生成select语句(包含表明、列名和查询条件都是通过参数动态传入的)。
+
+
+
diff --git a/section-15-dynamic-sql/pom.xml b/section-15-dynamic-sql/pom.xml
new file mode 100644
index 0000000..8d8680f
--- /dev/null
+++ b/section-15-dynamic-sql/pom.xml
@@ -0,0 +1,76 @@
+
+
+ 4.0.0
+
+ cn.devmgr.tutorial
+ tutorial-section15
+ 0.0.1-SNAPSHOT
+ jar
+
+ tutorial-15
+ 用Spring Boot编写RESTful API教程 02
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.0.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 1.3.2
+
+
+ org.postgresql
+ postgresql
+
+
+
+ commons-io
+ commons-io
+ 2.7
+
+
+
+
+ org.apache.poi
+ poi
+ 4.1.1
+
+
+ org.apache.poi
+ poi-ooxml
+ 3.17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/section-15-dynamic-sql/sql.sql b/section-15-dynamic-sql/sql.sql
new file mode 100644
index 0000000..d1a1e81
--- /dev/null
+++ b/section-15-dynamic-sql/sql.sql
@@ -0,0 +1,18 @@
+create database tvseries;
+
+/** TABLES **/
+create table tv_series(
+ id serial primary key,
+ name varchar(50) not null,
+ season_count int not null,
+ origin_release date not null,
+ status smallint not null default 0,
+ delete_reason varchar(100) null
+);
+
+create table tv_character(
+ id serial primary key,
+ tv_series_id int not null,
+ name varchar(50) not null,
+ photo varchar(100) null
+);
diff --git a/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/Application.java b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/Application.java
new file mode 100644
index 0000000..8eec9fc
--- /dev/null
+++ b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/Application.java
@@ -0,0 +1,15 @@
+package cn.devmgr.tutorial;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+@MapperScan("cn.devmgr.tutorial.dao")
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/DynamicTableLanguageDriver.java b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/DynamicTableLanguageDriver.java
new file mode 100644
index 0000000..d9d0787
--- /dev/null
+++ b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/DynamicTableLanguageDriver.java
@@ -0,0 +1,100 @@
+package cn.devmgr.tutorial;
+
+import org.apache.ibatis.executor.parameter.ParameterHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.ParameterMapping;
+import org.apache.ibatis.mapping.SqlSource;
+import org.apache.ibatis.parsing.XNode;
+import org.apache.ibatis.scripting.LanguageDriver;
+import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
+import org.apache.ibatis.session.Configuration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 自定义Mybatis LanguageDriver的例子
+ *
+ * mybatis首先调用createSqlSource方法,创建一个SqlSource(此时我们的程序尚未对mapper里的方法调用,可认为是mybatis初始化的过程;
+ * 不会传递mapper里参数的值),当我们程序调用mapper里的某个方法(方法的languageDriver是这个)会调用SqlSource.getBoundSql方法,并把具体的参数值
+ * 传递给getBoundSql方法,得到SqlBound后,会调用LanguageDriver.createParameterHandler
+ */
+public class DynamicTableLanguageDriver extends XMLLanguageDriver implements LanguageDriver {
+ private static final Logger logger = LoggerFactory.getLogger(DynamicTableLanguageDriver.class);
+
+ @Override
+ public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object o, BoundSql boundSql) {
+ logger.trace("createParameterHandler({}, Object: {}, BoundSql:{}", mappedStatement, o, boundSql.getSql());
+ logger.trace("Object: class:{}, isMap:{}", o.getClass().getName(), o instanceof Map);
+// logger.trace("")
+ logger.trace("boundSql: class: {}, sql: {}, getParameterMappings: {}, getParameterObject:{}",
+ boundSql.getClass().getName(), boundSql.getSql(), boundSql.getParameterMappings(), boundSql.getParameterObject());
+ return super.createParameterHandler(mappedStatement, o, boundSql);
+ }
+
+ @Override
+ public SqlSource createSqlSource(Configuration configuration, XNode xNode, Class> parameterType) {
+ logger.trace("createSqlSource({}, XNode: {}, {}", configuration, xNode, parameterType);
+ return super.createSqlSource(configuration, xNode, parameterType);
+ }
+
+ @Override
+ public SqlSource createSqlSource(Configuration configuration, String script, Class> parameterType) {
+ logger.trace("createSqlSource({}, script:{}, {}", configuration, script, parameterType);
+ return new DynamicTableSqlSource(configuration);
+ }
+
+
+ public static class DynamicTableSqlSource implements SqlSource{
+ private Configuration configuration;
+
+ public DynamicTableSqlSource(Configuration configuration){
+ this.configuration = configuration;
+ }
+
+ @Override
+ public BoundSql getBoundSql(Object o) {
+ logger.trace("DynamicTableSqlSource->getBoundSql object = class:{}, data:{}", o.getClass().getName(), o);
+ Map params = (Map) o;
+ StringBuffer sql = new StringBuffer();
+ sql.append("select ");
+ List columns = (List) params.get("columns");
+ for(int i=0; i 0){
+ sql.append(", ");
+ }
+ sql.append( columns.get(i));
+ }
+ sql.append(" from ").append(params.get("tableName"));
+ Map cons = null;
+ List parameterMappings = new ArrayList<>();
+ if(params.get("conditions") != null){
+ cons = (Map) params.get("conditions");
+ if(cons.size() > 0){
+ sql.append(" where ");
+ }
+ Set keys = cons.keySet();
+ boolean isFirst = true;
+ for(String k : keys){
+ if(!isFirst){
+ sql.append(" and ");
+ }
+ isFirst = false;
+ // 这里要用JDBC的占位符号'?',不再用mybatis #{}
+ sql.append(" ").append(k).append(" = ? ");
+
+ // 下面这行第二个参数,要对应mapper里面的@Param设置的参数名;
+ ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, "conditions." + k, cons.get(k).getClass());
+ parameterMappings.add(builder.build());
+ }
+ }
+ logger.trace("will create BoundSql with {}, {}, {}", sql.toString(), parameterMappings, cons);
+ return new BoundSql(configuration, sql.toString(), parameterMappings, cons);
+ }
+ }
+}
diff --git a/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/controller/SampleController.java b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/controller/SampleController.java
new file mode 100644
index 0000000..5aaae01
--- /dev/null
+++ b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/controller/SampleController.java
@@ -0,0 +1,139 @@
+package cn.devmgr.tutorial.controller;
+
+import org.apache.ibatis.javassist.tools.rmi.Sample;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.xssf.usermodel.XSSFRow;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Controller
+@RequestMapping("/sample")
+public class SampleController {
+ private static final Logger logger = LoggerFactory.getLogger(SampleController.class);
+
+
+ /**
+ * 导出Excel的例子
+ * 因为类上面注解是@Controller,此方法需要@ResponseBody注解;如果类是RestController,则不需要ResponseBody
+ * @return
+ * @throws Exception
+ */
+ @ResponseBody
+ @GetMapping("/export")
+ public ResponseEntity exportExcel() throws Exception{
+ logger.trace("exportExcel");
+ HttpHeaders responseHeaders = new HttpHeaders();
+ responseHeaders.setContentDispositionFormData("attachment",new String("导出的文件名.xlsx".getBytes(), "ISO8859-1"));
+ responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+
+ //中文文件名需要用iso8859-1编码
+ InputStream templateIs = this.getClass().getResourceAsStream("/excel-templates/templet.xlsx");
+ XSSFWorkbook workbook = new XSSFWorkbook(templateIs);
+ XSSFSheet sheet = workbook.getSheetAt(0);
+
+ List list = getDataList();
+
+ CellStyle cellStyle = workbook.createCellStyle();
+ CreationHelper createHelper = workbook.getCreationHelper();
+ cellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy/mm/dd"));
+
+ for (int i=0; i(bos.toByteArray(), responseHeaders, HttpStatus.OK);
+ }
+
+ private List getDataList(){
+ List data = new ArrayList<>();
+ Calendar c = Calendar.getInstance();
+ SampleItem si1 = new SampleItem("张三", c.getTime(), 98.5f);
+ c.add(Calendar.DAY_OF_YEAR, -872);
+ SampleItem si2 = new SampleItem("李四", c.getTime(), 82f);
+ data.add(si1);
+ data.add(si2);
+ return data;
+ }
+
+
+ /**
+ * 例子POJO
+ */
+ public class SampleItem{
+ private float score;
+ private Date date;
+ private String name;
+
+ public SampleItem(String name, Date date, float score){
+ this.name = name;
+ this.date = date;
+ this.score = score;
+ }
+
+ public float getScore() {
+ return score;
+ }
+
+ public void setScore(float score) {
+ this.score = score;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+
+}
+
+
diff --git a/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/dao/DynamicSqlDao.java b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/dao/DynamicSqlDao.java
new file mode 100644
index 0000000..0331084
--- /dev/null
+++ b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/dao/DynamicSqlDao.java
@@ -0,0 +1,30 @@
+package cn.devmgr.tutorial.dao;
+
+import cn.devmgr.tutorial.DynamicTableLanguageDriver;
+import org.apache.ibatis.annotations.Lang;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+
+@Repository
+public interface DynamicSqlDao {
+
+ /**
+ * 使用自定义LanguageDriver的例子,@Lang注解声明使用自定义的LanaguageDriver; 三个参数在自定义LanguageDriver中可以读取到,然后动态生成SQL
+ * @param tableName
+ * @param columns
+ * @param param
+ * @return
+ */
+ @Lang(DynamicTableLanguageDriver.class)
+ @Select("")
+ public List> getByConditions(@Param("tableName") String tableName,
+ @Param("columns") List columns,
+ @Param("conditions") Map param);
+
+
+}
diff --git a/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/service/SampleService.java b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/service/SampleService.java
new file mode 100644
index 0000000..58d0e0f
--- /dev/null
+++ b/section-15-dynamic-sql/src/main/java/cn/devmgr/tutorial/service/SampleService.java
@@ -0,0 +1,40 @@
+package cn.devmgr.tutorial.service;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import cn.devmgr.tutorial.dao.DynamicSqlDao;
+
+@Service
+public class SampleService {
+ private final Log log = LogFactory.getLog(SampleService.class);
+
+ @Autowired private DynamicSqlDao dynamicSqlDao;
+
+ @Transactional(readOnly=true)
+ public List> getAllTvSeries(){
+ if(log.isTraceEnabled()) {
+ log.trace("getAllTvSeries started ");
+ }
+
+ Map params = new HashMap<>();
+ params.put("id", 1);
+ List columns = new ArrayList<>();
+ columns.add("id");
+ columns.add("name");
+ String tableName = "tv_series";
+ List> list = dynamicSqlDao.getByConditions(tableName, columns, params);
+
+ return list;
+ }
+
+
+}
diff --git a/section-15-dynamic-sql/src/main/resources/application.yml b/section-15-dynamic-sql/src/main/resources/application.yml
new file mode 100644
index 0000000..36c2c0d
--- /dev/null
+++ b/section-15-dynamic-sql/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: spring-boot-tutorial
+ datasource:
+ driverClassName: org.postgresql.Driver
+ url: jdbc:postgresql://127.0.0.1:5432/tvseries?stringtype=unspecified&ApplicationName=tutorial
+ username: pgdbo
+ password: sql
+ jackson:
+ serialization:
+ write-dates-as-timestamps: true #使用数值timestamp表示日期
+
+mybatis.configuration.mapUnderscoreToCamelCase: true
+
+logging:
+ file: target/app.log
+ level:
+ ROOT: WARN
+ cn.devmgr: TRACE
+ cn.devmgr.tutorial.dao: WARN
+
+#此项目的自定义属性
+tutorial:
+ uploadFolder: target/upload-files/ #文件上传目录,可以用/开头的绝对路径
+
\ No newline at end of file
diff --git a/section-15-dynamic-sql/src/main/resources/excel-templates/templet.xlsx b/section-15-dynamic-sql/src/main/resources/excel-templates/templet.xlsx
new file mode 100644
index 0000000..db7a785
Binary files /dev/null and b/section-15-dynamic-sql/src/main/resources/excel-templates/templet.xlsx differ
diff --git a/section-15-dynamic-sql/src/test/java/cn/devmgr/tutorial/AppTests.java b/section-15-dynamic-sql/src/test/java/cn/devmgr/tutorial/AppTests.java
new file mode 100644
index 0000000..6a11ece
--- /dev/null
+++ b/section-15-dynamic-sql/src/test/java/cn/devmgr/tutorial/AppTests.java
@@ -0,0 +1,16 @@
+package cn.devmgr.tutorial;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class AppTests {
+
+ @Test
+ public void contextLoads() throws Exception {
+ }
+
+}
diff --git a/section-15-dynamic-sql/src/test/java/cn/devmgr/tutorial/MybatisLangTest.java b/section-15-dynamic-sql/src/test/java/cn/devmgr/tutorial/MybatisLangTest.java
new file mode 100644
index 0000000..231f563
--- /dev/null
+++ b/section-15-dynamic-sql/src/test/java/cn/devmgr/tutorial/MybatisLangTest.java
@@ -0,0 +1,40 @@
+package cn.devmgr.tutorial;
+
+import cn.devmgr.tutorial.dao.DynamicSqlDao;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class MybatisLangTest {
+ private static final Logger logger = LoggerFactory.getLogger(MybatisLangTest.class);
+
+ @Autowired
+ DynamicSqlDao tvSeriesDao;
+
+ @Test
+ public void testLang(){
+ logger.trace("test mybatis lang.....");
+ Map params = new HashMap<>();
+ params.put("id", 1);
+ List columns = new ArrayList<>();
+ columns.add("id");
+ columns.add("name");
+ List> list = tvSeriesDao.getByConditions("tv_series", columns, params);
+ logger.trace("dao return {}", list == null ? "null" : list.size());
+ if(list.size() > 0){
+ logger.trace("row 1: {}", list.get(0));
+ }
+ }
+}
diff --git a/section-16-email/README.md b/section-16-email/README.md
new file mode 100644
index 0000000..e0bc2d7
--- /dev/null
+++ b/section-16-email/README.md
@@ -0,0 +1,38 @@
+README
+===========================
+
+用Spring中提供的JavaMailSender发送邮件的例子
+
+
+## 加入依赖
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+```
+
+
+## 配置application.yml
+
+application.yml中需要配置发送邮件的SMTP的信息
+
+```yml
+spring:
+ mail:
+ host: smtp主机地址,例如网易域名邮箱是:smtp.ym.163.com;qq邮箱是smtp.qq.com
+ username: smtp登录的账号,一般是邮件地址
+ password: smtp登录账号对应的密码,一般和自己邮箱密码相同
+ properties:
+ mail:
+ smtp:
+ auth: true
+```
+
+## 写代码发送
+
+可参考[MailService](.src/main/java/cn/devmgr/tutorial/MailService.java)
+
+此法相比直接调用javamail发送邮件,稍微简单些。
+
diff --git a/section-16-email/pom.xml b/section-16-email/pom.xml
new file mode 100644
index 0000000..5677add
--- /dev/null
+++ b/section-16-email/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ cn.devmgr.tutorial
+ tutorial-section16
+ 0.0.1-SNAPSHOT
+ jar
+
+ tutorial-16-sendemail
+ 用Spring Boot编写RESTful API教程 16
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.0.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/section-16-email/src/main/java/cn/devmgr/tutorial/Application.java b/section-16-email/src/main/java/cn/devmgr/tutorial/Application.java
new file mode 100644
index 0000000..f273ded
--- /dev/null
+++ b/section-16-email/src/main/java/cn/devmgr/tutorial/Application.java
@@ -0,0 +1,12 @@
+package cn.devmgr.tutorial;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/section-16-email/src/main/java/cn/devmgr/tutorial/MailService.java b/section-16-email/src/main/java/cn/devmgr/tutorial/MailService.java
new file mode 100644
index 0000000..a7d7ab1
--- /dev/null
+++ b/section-16-email/src/main/java/cn/devmgr/tutorial/MailService.java
@@ -0,0 +1,106 @@
+package cn.devmgr.tutorial;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.InputStreamSource;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class MailService {
+
+ @Autowired
+ private JavaMailSender mailSender;
+
+ @Value("${spring.mail.username}") String sender;
+
+ /**
+ * 发送一个纯文本的最简单的邮件
+ * @param to
+ * @param subject
+ * @param body
+ */
+ public void sendEmail(String to, String subject, String body){
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(sender);
+ message.setTo(to);
+ message.setSubject(subject);
+ message.setText(body);
+
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送HTML格式的邮件
+ * @param to
+ * @param subject
+ * @param body
+ * @throws MessagingException
+ */
+ public void sendHtmlEmail(String to, String subject, String body) throws MessagingException {
+ MimeMessage mimeMessage = mailSender.createMimeMessage();
+ mimeMessage.setContent(body, "text/html");
+ MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false);
+ helper.setFrom(sender);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(body, true);
+
+ mailSender.send(mimeMessage);
+ }
+
+ /**
+ * 发送HTML格式,带有附件的邮件
+ * @param to
+ * @param subject
+ * @param body
+ * @param attachmentName
+ * @param iss
+ * @throws MessagingException
+ */
+ public void sendHtmlEmailWithAttachment(String to, String subject, String body, String attachmentName, InputStreamSource iss) throws MessagingException {
+ MimeMessage mimeMessage = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
+ helper.setFrom(sender);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(body, true);
+ // 可以增加多个附件,每个附件调用addAttachement
+ helper.addAttachment(attachmentName, iss);
+
+ mailSender.send(mimeMessage);
+
+ }
+
+
+ private String getMailBodyFromTemplate(String templateName, Map vars){
+ ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
+ resolver.setPrefix("email-templates/"); //模板所在目录,相对于当前classloader的classpath。
+ resolver.setSuffix(".html"); //模板文件后缀
+ TemplateEngine templateEngine = new TemplateEngine();
+ templateEngine.setTemplateResolver(resolver);
+
+ Context context = new Context();
+ for(Iterator iterator = vars.keySet().iterator(); iterator.hasNext();){
+ String key = iterator.next();
+ context.setVariable(key, vars.get(key));
+ }
+
+ StringWriter writer = new StringWriter();
+ templateEngine.process("example", context, writer);
+ return writer.toString();
+ }
+}
diff --git a/section-16-email/src/main/java/cn/devmgr/tutorial/SampleController.java b/section-16-email/src/main/java/cn/devmgr/tutorial/SampleController.java
new file mode 100644
index 0000000..9dbb2a7
--- /dev/null
+++ b/section-16-email/src/main/java/cn/devmgr/tutorial/SampleController.java
@@ -0,0 +1,21 @@
+package cn.devmgr.tutorial;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/sample")
+public class SampleController {
+
+ @GetMapping
+ public List getAll() {
+ List list = new ArrayList<>();
+
+ return list;
+ }
+}
diff --git a/section-16-email/src/main/resources/.gitignore b/section-16-email/src/main/resources/.gitignore
new file mode 100644
index 0000000..a0773a4
--- /dev/null
+++ b/section-16-email/src/main/resources/.gitignore
@@ -0,0 +1,2 @@
+application-dev.yml
+
diff --git a/section-16-email/src/main/resources/application.yml b/section-16-email/src/main/resources/application.yml
new file mode 100644
index 0000000..4b369cb
--- /dev/null
+++ b/section-16-email/src/main/resources/application.yml
@@ -0,0 +1,10 @@
+
+spring:
+ mail:
+ host: smtp主机地址,例如网易域名邮箱是:smtp.ym.163.com;qq邮箱是smtp.qq.com
+ username: smtp登录的账号,一般是邮件地址
+ password: smtp登录账号对应的密码,一般和自己邮箱密码相同
+ properties:
+ mail:
+ smtp:
+ auth: true
diff --git a/section-16-email/src/test/java/cn/devmgr/tutorial/TutorialApplicationTests.java b/section-16-email/src/test/java/cn/devmgr/tutorial/TutorialApplicationTests.java
new file mode 100644
index 0000000..9ed6793
--- /dev/null
+++ b/section-16-email/src/test/java/cn/devmgr/tutorial/TutorialApplicationTests.java
@@ -0,0 +1,30 @@
+package cn.devmgr.tutorial;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class TutorialApplicationTests {
+
+ @Autowired MailService mailService;
+
+ @Test
+ public void contextLoads() {
+ }
+
+ @Test
+ public void sendMailTest() throws Exception{
+ String body = "hi sir\r\n\r\nkonichiha. \r\n\r\n best wishes.\r\n\r\nsincerely yours,\r\n me";
+ mailService.sendEmail("someone@somewhere.com", "邮件主题-Text", body);
+
+ String htmlBody = "hi sir,hello.
best wishes. sincerely yours, me";
+ mailService.sendHtmlEmail("someone@somewhere.com", "邮件主题-HTML", htmlBody);
+
+ mailService.sendHtmlEmailWithAttachment("someone@somewhere.com", "邮件主题-附件", htmlBody, "附件.yml", new ClassPathResource("/application.yml"));
+ }
+}
diff --git a/section-17-bpm-activiti/README.md b/section-17-bpm-activiti/README.md
new file mode 100644
index 0000000..8a988c2
--- /dev/null
+++ b/section-17-bpm-activiti/README.md
@@ -0,0 +1,24 @@
+# Spring boot 集成 activiti
+
+## 名词解释
+
+* BPM (Business Process Management) 业务流程管理。
+* BPMN (Business Process Model and Notation) 业务流程模型和标记法。是一套图形化表示法,用于以业务流程模型详细说明各种业务流程。 目前版本为2.0,由 [OMG](https://zh.wikipedia.org/wiki/%E5%AF%B9%E8%B1%A1%E7%AE%A1%E7%90%86%E7%BB%84%E7%BB%87) 进行维护。
+* Activiti 开源 BPM 引擎, 网站 http://www.activiti.org 。
+
+
+
+## 本教程介绍
+
+* [初始化项目环境](init.md)
+
+
+
+
+## 官方文档
+
+[https://www.activiti.org/userguide/](https://www.activiti.org/userguide/)
+
+
+
+
diff --git a/section-17-bpm-activiti/activiti-form.md b/section-17-bpm-activiti/activiti-form.md
new file mode 100644
index 0000000..ebfeb9a
--- /dev/null
+++ b/section-17-bpm-activiti/activiti-form.md
@@ -0,0 +1,44 @@
+# FORM
+
+FormService是可选的
+
+
+User forms are not part of BPMN standard.
+
+As you can see tags related to forms are located under extensionElements tag and are belonging to the specific namespace used with the prefix activiti.
+
+extensionElements tag is part of BPMN standard (see section 8.2.3 of BPMN standard 2.0.2 or XML schema) and its purpose is to allow vendor specific extension to the standard (such as forms).
+
+
+
+The new Activiti 6 UI uses a different way do display forms. It includes a Form Designer that enables designing forms by dragging and dropping fields. Forms are then referenced using the activiti:formKey attribute in start events and user tasks.
+
+
+增加一个表单
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+* Form property room will be mapped to process variable room as a String
+* Form property duration will be mapped to process variable duration as a java.lang.Long
+* Form property speaker will be mapped to process variable SpeakerName. It will only be available in the TaskFormData object. If property speaker is submitted, an ActivitiException will be thrown. Analogue, with attribute readable="false", a property can be excluded from the FormData, but still be processed in the submit.
+* Form property street will be mapped to Java bean property street in process variable address as a String. And required="true" will throw an exception during the submit if the property is not provided.
+
+It’s also possible to provide type metadata as part of the FormData that is returned from methods StartFormData FormService.getStartFormData(String processDefinitionId) and TaskFormdata FormService.getTaskFormData(String taskId)
+
+We support the following form property types:
+
+* string (org.activiti.engine.impl.form.StringFormType
+* long (org.activiti.engine.impl.form.LongFormType)
+* enum (org.activiti.engine.impl.form.EnumFormType)
+* date (org.activiti.engine.impl.form.DateFormType)
+* boolean (org.activiti.engine.impl.form.BooleanFormType)
+
diff --git a/section-17-bpm-activiti/activiti-objects.md b/section-17-bpm-activiti/activiti-objects.md
new file mode 100644
index 0000000..302b371
--- /dev/null
+++ b/section-17-bpm-activiti/activiti-objects.md
@@ -0,0 +1,27 @@
+# Activiti 核心组件
+
+## 关键对象
+
+* Deployment:流程部署对象,部署一个流程时创建。
+* ProcessDefinitions:流程定义,部署成功后自动创建。
+* ProcessInstances:流程实例,启动流程时创建。
+* Task:任务,在Activiti中的Task仅指有角色参与的任务,即定义中的UserTask。
+* Execution:执行计划,流程实例和流程执行中的所有节点都是Execution,如UserTask、ServiceTask等。
+
+## 服务接口
+
+* ProcessEngine:流程引擎的抽象,通过它我们可以获得我们需要的一切服务。
+* RepositoryService:Activiti中每一个不同版本的业务流程的定义都需要使用一些定义文件,部署文件和支持数据(例如BPMN2.0 XML文件,表单定义文件,流程定义图像文件等),这些文件都存储在Activiti内建的Repository中。RepositoryService提供了对 repository的存取服务。
+* RuntimeService:在Activiti中,每当一个流程定义被启动一次之后,都会生成一个相应的流程对象实例。RuntimeService提供了启动流程、查询流程实例、设置获取流程实例变量等功能。此外它还提供了对流程部署,流程定义和流程实例的存取服务。
+* TaskService: 在Activiti中业务流程定义中的每一个执行节点被称为一个Task,对流程中的数据存取,状态变更等操作均需要在Task中完成。TaskService提供了对用户Task 和Form相关的操作。它提供了运行时任务查询、领取、完成、删除以及变量设置等功能。
+ * 查询分配给用户或组的任务
+ * 创建独立运行任务。这些任务与流程实例无关。
+ * 手工设置任务的执行者,或者这些用户通过何种方式与任务关联。
+ * 认领并完成一个任务。认领意味着一个人期望成为任务的执行者, 即这个用户会完成这个任务。完成意味着“做这个任务要求的事情”。 通常来说会有很多种处理形式。
+* IdentityService: Activiti中内置了用户以及组管理的功能,必须使用这些用户和组的信息才能获取到相应的Task。IdentityService提供了对Activiti 系统中的用户和组的管理功能。它可以管理(创建,更新,删除,查询...)群组和用户。 请注意, Activiti执行时并没有对用户进行检查。 例如,任务可以分配给任何人,但是引擎不会校验系统中是否存在这个用户
+* ManagementService: ManagementService提供了对Activiti流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于Activiti系统的日常维护。 ManagementService在使用Activiti的定制环境中基本上不会用到。 它可以查询数据库的表和表的元数据。另外,它提供了查询和管理异步操作的功能。 Activiti的异步操作用途很多,比如定时器,异步操作, 延迟暂停、激活,等等。后续,会讨论这些功能的更多细节。
+* HistoryService: HistoryService用于获取正在运行或已经完成的流程实例的信息,与RuntimeService中获取的流程信息不同,历史信息包含已经持久化存储的永久信息,并已经被针对查询优化。HistoryService提供了Activiti引擎手机的所有历史数据。 在执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。 这个服务主要通过查询功能来获得这些数据。
+* FormService: Activiti 中的流程和状态 Task 均可以关联业务相关的数据。通过使用 Form Service 可以存取启动和完成任务所需的表单数据并且根据需要来渲染表单。FormService是一个可选服务。即使不使用它,Activiti也可以完美运行, 不会损失任何功能。这个服务提供了启动表单和任务表单两个概念。 启动表单会在流程实例启动之前展示给用户, 任务表单会在用户完成任务时展示。Activiti支持在BPMN 2.0流程定义中设置这些表单。 这个服务以一种简单的方式将数据暴露出来。再次重申,它时可选的, 表单也不一定要嵌入到流程定义中。
+
+
+##
\ No newline at end of file
diff --git a/section-17-bpm-activiti/init.md b/section-17-bpm-activiti/init.md
new file mode 100644
index 0000000..2d3f554
--- /dev/null
+++ b/section-17-bpm-activiti/init.md
@@ -0,0 +1,230 @@
+# 搭建项目环境
+
+本节使用的是spring boot 2.1.2, activiti 6.0.0 (两者都是目前最高版本),而activiti-spring-boot 依旧在使用spring boot 1.x 版本中的spring security 4.x, spring boot 2.x 则集成了spring security 5.x,两者有些差异,因此本节有些内容是为了消除这些差异而写的代码/设置; 在将来activiti升级后应该不再需要。
+
+
+
+## POM
+
+增加依赖并设置版本
+
+```xml
+
+
+ 6.0.0
+
+
+
+
+
+
+
+ org.activiti
+ activiti-spring-boot-starter-basic
+ ${activiti.version}
+
+
+ org.activiti
+ activiti-spring-boot-starter-rest-api
+ ${activiti.version}
+
+
+
+
+ org.postgresql
+ postgresql
+ 42.2.5
+
+
+
+
+```
+
+
+### 在src/main/resources目录创建配置
+
+在src/main/resources目录下创建一个 processes 目录,并且放置一个 .bmpn 的流程配置文件(文件名任意),如果没有这个文件,启动时会报错。
+
+ Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.activiti.spring.SpringProcessEngineConfiguration]: Factory method 'springProcessEngineConfiguration' threw exception; nested exception is java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
+ at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:579) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ ... 59 common frames omitted
+ Caused by: java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
+ at org.springframework.core.io.ClassPathResource.getURL(ClassPathResource.java:195) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.core.io.support.PathMatchingResourcePatternResolver.findPathMatchingResources(PathMatchingResourcePatternResolver.java:495) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.core.io.support.PathMatchingResourcePatternResolver.getResources(PathMatchingResourcePatternResolver.java:296) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.support.AbstractApplicationContext.getResources(AbstractApplicationContext.java:1305) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.support.GenericApplicationContext.getResources(GenericApplicationContext.java:238) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.activiti.spring.boot.AbstractProcessEngineConfiguration.discoverProcessDefinitionResources(AbstractProcessEngineConfiguration.java:79) ~[activiti-spring-boot-starter-basic-6.0.0.jar:na]
+ at org.activiti.spring.boot.AbstractProcessEngineAutoConfiguration.baseSpringProcessEngineConfiguration(AbstractProcessEngineAutoConfiguration.java:74) ~[activiti-spring-boot-starter-basic-6.0.0.jar:na]
+ at org.activiti.spring.boot.DataSourceProcessEngineAutoConfiguration$DataSourceProcessEngineConfiguration.springProcessEngineConfiguration(DataSourceProcessEngineAutoConfiguration.java:57) ~[activiti-spring-boot-starter-basic-6.0.0.jar:na]
+ at org.activiti.spring.boot.DataSourceProcessEngineAutoConfiguration$DataSourceProcessEngineConfiguration$$EnhancerBySpringCGLIB$$30cb9c3e.CGLIB$springProcessEngineConfiguration$1() ~[activiti-spring-boot-starter-basic-6.0.0.jar:na]
+ at org.activiti.spring.boot.DataSourceProcessEngineAutoConfiguration$DataSourceProcessEngineConfiguration$$EnhancerBySpringCGLIB$$30cb9c3e$$FastClassBySpringCGLIB$$9fbdb05e.invoke() ~[activiti-spring-boot-starter-basic-6.0.0.jar:na]
+ at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:361) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.activiti.spring.boot.DataSourceProcessEngineAutoConfiguration$DataSourceProcessEngineConfiguration$$EnhancerBySpringCGLIB$$30cb9c3e.springProcessEngineConfiguration() ~[activiti-spring-boot-starter-basic-6.0.0.jar:na]
+ at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
+ at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
+ at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
+ at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
+ at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ ... 60 common frames omitted
+
+无法启动
+
+
+### 修改启动注解 @SpringBootAppliction
+
+从 `@SpringBootApplication` 改为
+
+```Java
+@SpringBootApplication(exclude=org.activiti.spring.boot.SecurityAutoConfiguration.class)
+```
+这是为了禁止 activiti 的自动配置的安全机制,否则会出现如下错误而无法启动.
+
+
+ org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [cn.devmgr.tutorial.Application]; nested exception is java.io.FileNotFoundException: class path resource [org/springframework/security/config/annotation/authentication/configurers/GlobalAuthenticationConfigurerAdapter.class] cannot be opened because it does not exist
+ at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:616) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:548) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:184) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:316) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:273) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:93) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:693) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:531) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) [spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:388) [spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) [spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) [spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:1234) [spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at cn.devmgr.tutorial.Application.main(Application.java:15) [classes/:na]
+ Caused by: java.io.FileNotFoundException: class path resource [org/springframework/security/config/annotation/authentication/configurers/GlobalAuthenticationConfigurerAdapter.class] cannot be opened because it does not exist
+ at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:180) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.core.type.classreading.SimpleMetadataReader.(SimpleMetadataReader.java:51) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:103) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory.createMetadataReader(ConcurrentReferenceCachingMetadataReaderFactory.java:88) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory.getMetadataReader(ConcurrentReferenceCachingMetadataReaderFactory.java:75) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
+ at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:81) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.asSourceClass(ConfigurationClassParser.java:699) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser$SourceClass.getSuperClass(ConfigurationClassParser.java:869) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:326) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:241) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.processMemberClasses(ConfigurationClassParser.java:355) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:261) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:241) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:606) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
+ ... 15 common frames omitted
+
+
+出现上面的错误,是因为spring boot 2.0使用spring security 5.x;而activiti使用spring security 4.x。
+而spring security 4.x -> 5.x的升级中有些package名字改变了。
+
+如果需要使用 Activiti 的安全机制,可参考[https://www.baeldung.com/activiti-spring-security](https://www.baeldung.com/activiti-spring-security)进行配置。
+
+
+### 设置Spring security
+
+需要设置 spring security, 并增加注解 `@EnableGlobalMethodSecurity(prePostEnabled = true)`
+
+如果没有设置 `prePostEnabled = true` 会出现如下异常,不能启动
+
+ org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.hateoas.config.HateoasConfiguration': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'metaDataSourceAdvisor': Cannot resolve reference to bean 'methodSecurityMetadataSource' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'methodSecurityMetadataSource' defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.access.method.MethodSecurityMetadataSource]: Factory method 'methodSecurityMetadataSource' threw exception; nested exception is java.lang.IllegalStateException: In the composition of all global method configuration, no annotation support was actually activated
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:584) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:392) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1288) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1127) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.context.support.PostProcessorRegistrationDelegate.registerBeanPostProcessors(PostProcessorRegistrationDelegate.java:240) ~[spring-context-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:707) ~[spring-context-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:531) ~[spring-context-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
+ at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
+ at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
+ at cn.devmgr.tutorial.bpm.Application.main(Application.java:23) [classes/:na]
+ Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'metaDataSourceAdvisor': Cannot resolve reference to bean 'methodSecurityMetadataSource' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'methodSecurityMetadataSource' defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.access.method.MethodSecurityMetadataSource]: Factory method 'methodSecurityMetadataSource' threw exception; nested exception is java.lang.IllegalStateException: In the composition of all global method configuration, no annotation support was actually activated
+ at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:378) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:110) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:662) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:188) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1308) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1154) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.aop.framework.autoproxy.BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans(BeanFactoryAdvisorRetrievalHelper.java:91) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findCandidateAdvisors(AbstractAdvisorAutoProxyCreator.java:109) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(AbstractAdvisorAutoProxyCreator.java:94) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean(AbstractAdvisorAutoProxyCreator.java:76) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:349) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:301) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:434) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1749) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:576) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ ... 24 common frames omitted
+ Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'methodSecurityMetadataSource' defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.access.method.MethodSecurityMetadataSource]: Factory method 'methodSecurityMetadataSource' threw exception; nested exception is java.lang.IllegalStateException: In the composition of all global method configuration, no annotation support was actually activated
+ at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:627) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:456) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1288) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1127) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:367) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ ... 44 common frames omitted
+ Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.access.method.MethodSecurityMetadataSource]: Factory method 'methodSecurityMetadataSource' threw exception; nested exception is java.lang.IllegalStateException: In the composition of all global method configuration, no annotation support was actually activated
+ at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:622) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ ... 54 common frames omitted
+ Caused by: java.lang.IllegalStateException: In the composition of all global method configuration, no annotation support was actually activated
+ at org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration.methodSecurityMetadataSource(GlobalMethodSecurityConfiguration.java:368) ~[spring-security-config-5.1.3.RELEASE.jar:5.1.3.RELEASE]
+ at org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration$$EnhancerBySpringCGLIB$$9ca90329.CGLIB$methodSecurityMetadataSource$10() ~[spring-security-config-5.1.3.RELEASE.jar:5.1.3.RELEASE]
+ at org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration$$EnhancerBySpringCGLIB$$9ca90329$$FastClassBySpringCGLIB$$80529ebe.invoke() ~[spring-security-config-5.1.3.RELEASE.jar:5.1.3.RELEASE]
+ at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363) ~[spring-context-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ at org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration$$EnhancerBySpringCGLIB$$9ca90329.methodSecurityMetadataSource() ~[spring-security-config-5.1.3.RELEASE.jar:5.1.3.RELEASE]
+ at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
+ at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
+ at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
+ at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
+ at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE]
+ ... 55 common frames omitted
+
+
+## 数据库内的表
+
+ 正常启动后,会在数据库内创建28张以 act_ 开头的表
+
+### 表的分类
+
+数据库内表都以 act_xx_yyy 的形式命名,xx是分类
+ The second part is a two-character identification of
+ the use case of the table. This use case will also roughly match the service API.
+
+* ACT_RE_*: RE stands for repository. Tables with this prefix contain static information such as process definitions and process resources (images, rules, etc.).
+* ACT_RU_*: RU stands for runtime. These are the runtime tables that contain the runtime data of process instances, user tasks, variables, jobs, etc. Activiti only stores the runtime data during process instance execution, and removes the records when a process instance ends. This keeps the runtime tables small and fast.
+* ACT_ID_*: ID stands for identity. These tables contain identity information, such as users, groups, etc.
+* ACT_HI_*: HI stands for history. These are the tables that contain historic data, such as past process instances, variables, tasks, etc.
+* ACT_GE_*: general data, which is used in various use cases.
+
+
diff --git a/section-17-bpm-activiti/pom.xml b/section-17-bpm-activiti/pom.xml
new file mode 100644
index 0000000..1cfee5d
--- /dev/null
+++ b/section-17-bpm-activiti/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+ cn.devmgr.tutorial
+ tutorial-section17-activiti
+ 0.0.1-SNAPSHOT
+ jar
+
+ tutorial-16-activiti
+ 用Spring Boot编写RESTful API教程 17 (使用Activiti)
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.2.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 6.0.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+
+ org.postgresql
+ postgresql
+ 42.4.1
+
+
+
+ org.activiti
+ activiti-spring-boot-starter-basic
+ ${activiti.version}
+
+
+ org.activiti
+ activiti-spring-boot-starter-rest-api
+ ${activiti.version}
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+
diff --git a/section-17-bpm-activiti/rest-api.md b/section-17-bpm-activiti/rest-api.md
new file mode 100644
index 0000000..85b2e4f
--- /dev/null
+++ b/section-17-bpm-activiti/rest-api.md
@@ -0,0 +1,20 @@
+# REST API
+
+## 列出所有任务
+
+``` bash
+curl http://localhost:8080/runtime/tasks
+```
+
+## 列出所有流程
+
+```bash
+curl http://localhost:8080/repository/process-definitions
+```
+
+## 创建新 hireProcess 流程
+
+```bash
+curl -H "Content-Type: application/json" -d '{"processDefinitionKey":"hireProcess", "variables": [ {"name":"applicantName", "value":"waytt"}, {"name":"email", "value":"waytt@westworld.com"}, {"name":"phoneNumber", "value":"13312344321"} ]}' http://localhost:8080/runtime/process-instances
+
+```
diff --git a/section-17-bpm-activiti/sql.sql b/section-17-bpm-activiti/sql.sql
new file mode 100644
index 0000000..f379cfc
--- /dev/null
+++ b/section-17-bpm-activiti/sql.sql
@@ -0,0 +1,3 @@
+create database bpm;
+
+
diff --git a/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/Application.java b/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/Application.java
new file mode 100644
index 0000000..1836d90
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/Application.java
@@ -0,0 +1,18 @@
+package cn.devmgr.tutorial.bpm;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+/**
+ * 此处禁止了activiti的默认spring security设置,因为它使用的是spring security 4.x部分,在
+ * spring-boot 2.x中不能生效
+ */
+@SpringBootApplication(exclude=org.activiti.spring.boot.SecurityAutoConfiguration.class)
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/ResumeService.java b/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/ResumeService.java
new file mode 100644
index 0000000..51c9bc6
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/ResumeService.java
@@ -0,0 +1,16 @@
+package cn.devmgr.tutorial.bpm;
+
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ResumeService {
+ private static final Logger logger = LoggerFactory.getLogger(ResumeService.class);
+
+
+ public void storeResume(){
+ logger.trace("storeResume()................");
+ }
+}
diff --git a/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/WebSecurityConfig.java b/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/WebSecurityConfig.java
new file mode 100644
index 0000000..9499a87
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/java/cn/devmgr/tutorial/bpm/WebSecurityConfig.java
@@ -0,0 +1,33 @@
+package cn.devmgr.tutorial.bpm;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+
+/**
+ * actitivi-spring-boot 会引入spring-security包,默认阻止了api的访问(需要登录授权后才能访问),此处设置任何api
+ * 都不需要登录授权即可访问,只是为了降低例子的复杂度,专注activiti的流程管理部分
+ */
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+//@EnableGlobalMethodSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+ private final static Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
+
+ @Override
+ protected void configure(HttpSecurity httpSecurity) throws Exception {
+ logger.trace("configure of spring security");
+
+ //默认的spring-security配置会让所有的请求都必须在已登录的状况下访问;下面这段代码禁止了这种操作。
+ httpSecurity.csrf().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+ .authorizeRequests().anyRequest().permitAll();
+ }
+
+}
\ No newline at end of file
diff --git a/section-17-bpm-activiti/src/main/resources/.gitignore b/section-17-bpm-activiti/src/main/resources/.gitignore
new file mode 100644
index 0000000..a0773a4
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/resources/.gitignore
@@ -0,0 +1,2 @@
+application-dev.yml
+
diff --git a/section-17-bpm-activiti/src/main/resources/application.yml b/section-17-bpm-activiti/src/main/resources/application.yml
new file mode 100644
index 0000000..e5fbf7c
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/resources/application.yml
@@ -0,0 +1,27 @@
+server:
+ port: 8080
+
+spring:
+ jackson:
+ date-format: yyyy-MM-dd
+ time-zone: GMT+8
+ serialization:
+ write-dates-as-timestamps: true #使用数值timestamp表示日期,true时会忽略date-format
+ datasource:
+ driverClassName: org.postgresql.Driver
+ url: jdbc:postgresql://127.0.0.1:5432/bpm?stringtype=unspecified&ApplicationName=tutorial
+ username: pgdbo
+ password: sql
+ activiti:
+ mail-server-host: smtp.ym.163.com
+ mail-server-user-name: user@user.com
+ mail-server-password: password
+ mail-server-default-from: user@user.com
+
+
+logging:
+ file: target/app.log
+ level:
+ ROOT: WARN
+ cn.devmgr: TRACE
+
diff --git a/section-17-bpm-activiti/src/main/resources/processes/hire.bpmn b/section-17-bpm-activiti/src/main/resources/processes/hire.bpmn
new file mode 100644
index 0000000..4623ef8
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/resources/processes/hire.bpmn
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/section-17-bpm-activiti/src/main/resources/processes/tt.bpmn b/section-17-bpm-activiti/src/main/resources/processes/tt.bpmn
new file mode 100644
index 0000000..be1e686
--- /dev/null
+++ b/section-17-bpm-activiti/src/main/resources/processes/tt.bpmn
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/section-17-bpm-activiti/src/test/java/cn/devmgr/tutorial/bpm/test/AppTest.java b/section-17-bpm-activiti/src/test/java/cn/devmgr/tutorial/bpm/test/AppTest.java
new file mode 100644
index 0000000..2e6121c
--- /dev/null
+++ b/section-17-bpm-activiti/src/test/java/cn/devmgr/tutorial/bpm/test/AppTest.java
@@ -0,0 +1,140 @@
+package cn.devmgr.tutorial.bpm.test;
+
+
+import org.activiti.engine.FormService;
+import org.activiti.engine.HistoryService;
+import org.activiti.engine.IdentityService;
+import org.activiti.engine.RuntimeService;
+import org.activiti.engine.form.FormProperty;
+import org.activiti.engine.form.TaskFormData;
+import org.activiti.engine.history.HistoricProcessInstance;
+import org.activiti.engine.identity.Group;
+import org.activiti.engine.identity.User;
+import org.activiti.engine.task.Task;
+import org.activiti.engine.TaskService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Profile;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertTrue;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@Profile("dev")
+public class AppTest {
+ private static final Logger logger = LoggerFactory.getLogger(AppTest.class);
+
+ @Autowired(required = false)
+ TaskService taskService;
+
+ @Autowired(required = false)
+ IdentityService identityService;
+
+ @Autowired(required = false)
+ RuntimeService runtimeService;
+
+ @Autowired(required = false)
+ HistoryService historyService;
+
+ @Autowired(required = false)
+ FormService formService;
+
+ @Test
+ public void listTasks() {
+ // 1. 查寻/设置 用户和组
+ logger.trace("query and create managers group and user waytt if they are not exist.");
+ String groupId = "hosts";
+ String userId = "dolores";
+ //查询有没有ID为 managers 的组
+ List hostGroups = identityService.createGroupQuery().groupId(groupId).list();
+ Group hostGroup;
+ if(hostGroups == null || hostGroups.size() == 0) {
+ // 没有,增加一个
+ hostGroup = identityService.newGroup(groupId); //参数是groupId
+ hostGroup.setName("接待员");
+ hostGroup.setType("business-role");
+ identityService.saveGroup(hostGroup);
+ }else{
+ hostGroup = hostGroups.get(0);
+ }
+
+ List users = identityService.createUserQuery().userId(userId).list();
+ User userDolores;
+ if(users == null || users.size() == 0) {
+ userDolores = identityService.newUser(userId);
+ userDolores.setFirstName("Dolores");
+ userDolores.setLastName("Abernathy");
+ userDolores.setPassword("none");
+ userDolores.setEmail("dolores@westworld.com");
+ identityService.saveUser(userDolores);
+ identityService.createMembership(userDolores.getId(), hostGroup.getId());
+ }else{
+ userDolores = users.get(0);
+ }
+
+
+ logger.trace("listTasks");
+ List tasks = taskService.createTaskQuery().list();
+
+ if(tasks == null || tasks.size() == 0){
+ logger.trace("donot found any tasks; add one");
+
+ Map variables = new HashMap();
+ variables.put("applicantName", "Bernard Lowe");
+ variables.put("email", "bernard@westworld.com");
+ variables.put("phoneNumber", "123456789");
+ runtimeService.startProcessInstanceByKey("hireProcess", variables);
+ }else{
+ for(Task t : tasks){
+ logger.trace(" task {}, {}, {}, {}, {}", t.getCategory(), t.getClaimTime(),
+ t.getName(), t.getId(), t.getFormKey());
+
+
+ TaskFormData taskFormData = formService.getTaskFormData(t.getId());
+ List formProperties = taskFormData.getFormProperties();
+ for (FormProperty property : formProperties) {
+ logger.trace(" FormData property id: {}, name: {}, type: {} value: {}", property.getId(), property.getName(), property.getType(), property.getValue());
+ }
+
+ }
+ }
+
+ //query again
+ tasks = taskService.createTaskQuery().list();
+ assertTrue(tasks.size() > 0);
+ for(int i=0; i tasksOfDolores = taskService.createTaskQuery().taskAssignee(userDolores.getId()).list();
+ for (Task dt : tasksOfDolores) {
+ logger.trace("Dolores's task: {}, {}, {}", dt.getId(), dt.getCreateTime(), dt.getProcessVariables());
+ // 设置此任务为完成状态
+ Map vars = new HashMap<>();
+ vars.put("telephoneInterviewOutcome", true);
+ vars.put("techOk", false);
+ vars.put("financialOk", true);
+ taskService.complete(dt.getId(), vars);
+
+ // 查询这个任务对应的流程的最终状态
+ HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery().processInstanceId(dt.getProcessInstanceId()).singleResult();
+ logger.trace("HistoricProcessInstance {}, {}, {}, {}", hpi.getName(), hpi.getEndActivityId(), hpi.getStartTime(), hpi.getEndTime());
+ }
+
+
+ }
+
+}
diff --git a/section-17-bpm-activiti/src/test/resources/blank.txt b/section-17-bpm-activiti/src/test/resources/blank.txt
new file mode 100644
index 0000000..e69de29
diff --git a/section-18-web-flow/README.md b/section-18-web-flow/README.md
new file mode 100644
index 0000000..4a04e38
--- /dev/null
+++ b/section-18-web-flow/README.md
@@ -0,0 +1,52 @@
+# Web Flow with SpringMVC
+
+Spring Web Flow 是在 SpringMVC 基础上用于做按照一定步骤的形式的一组页面。
+
+
+
+本例子用是一个简单的查看购物车、确认订单、创建订单、支付的流程。
+
+
+## 步骤
+
+### pom
+
+```xml
+
+ org.springframework.webflow
+ spring-webflow
+ 2.5.0.RELEASE
+
+
+ org.springframework.webflow
+ org.springframework.js
+ 2.0.5.RELEASE
+
+```
+
+webflow 需要SpringMVC,所以pom里应该有spring mvc和对应的模板(thymeleaf)的依赖。
+
+### 创建配置类
+
+[参照WebFlowConfig.java](./src/main/java/cn/devmgr/tutorial/webflow/WebFlowConfig.java)
+
+
+### 创建流程配置的xml
+
+[参照buy-flow.xml](./src/main/resources/flows/buy/buy-flow.xml)
+
+此类中可以使用spring的compent,执行里面的方法等,用springEL方式调用。
+
+### 创建流程中使用的模版
+
+[参数templates/flows/buy目录](./src/main/resources/templates/flows/buy)
+
+## ERROR
+
+所有的在流程里传递的POJO都需要实现 Serializable 接口,否则运行时出现异常。
+注意还要包含他们的成员变量的类、内部类等。
+
+ Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.webflow.execution.repository.snapshot.SnapshotCreationException: Could not serialize flow execution; make sure all objects stored in flow or flash scope are serializable] with root cause
+ java.io.NotSerializableException: cn.devmgr.tutorial.webflow.pojo.ShoppingCart
+
+
diff --git a/section-18-web-flow/pom.xml b/section-18-web-flow/pom.xml
new file mode 100644
index 0000000..eef1acc
--- /dev/null
+++ b/section-18-web-flow/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ cn.devmgr.tutorial
+ tutorial-section18
+ 0.0.1-SNAPSHOT
+ jar
+
+ tutorial-section18-web-flow
+ Demo project for Spring Boot
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.0.RELEASE
+
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.webflow
+ spring-webflow
+ 2.5.0.RELEASE
+
+
+ org.springframework.webflow
+ org.springframework.js
+ 2.0.5.RELEASE
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ nz.net.ultraq.thymeleaf
+ thymeleaf-layout-dialect
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/Application.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/Application.java
new file mode 100644
index 0000000..eb4cca9
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/Application.java
@@ -0,0 +1,14 @@
+package cn.devmgr.tutorial.webflow;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/BuyHandler.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/BuyHandler.java
new file mode 100644
index 0000000..b768cd0
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/BuyHandler.java
@@ -0,0 +1,96 @@
+package cn.devmgr.tutorial.webflow;
+
+import cn.devmgr.tutorial.webflow.pojo.Order;
+import cn.devmgr.tutorial.webflow.pojo.Product;
+import cn.devmgr.tutorial.webflow.pojo.ShoppingCart;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.binding.message.MessageBuilder;
+import org.springframework.binding.message.MessageContext;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * 在buy-flow.xml有配置用到此component
+ */
+@Component
+public class BuyHandler {
+ private final static Logger logger = LoggerFactory.getLogger(BuyHandler.class);
+
+
+ public ShoppingCart init(){
+ logger.trace("init()");
+
+ ShoppingCart cart = new ShoppingCart();
+
+ //初始化购物车类;一般需要从数据库或cookie读取内容到这个类
+ ShoppingCart.CartItem cartItemApple = cart.new CartItem();
+
+ Product prodApple = new Product();
+ prodApple.setId("A001");
+ prodApple.setName("红富士苹果");
+ cartItemApple.setProduct(prodApple);
+ cartItemApple.setNum(5);
+ cart.getCartItems().add(cartItemApple);
+
+ ShoppingCart.CartItem cartItemBanana = cart.new CartItem();
+ Product prodBanana = new Product();
+ prodBanana.setId("B0002");
+ prodBanana.setName("海南大香蕉");
+ cartItemBanana.setProduct(prodBanana);
+ cartItemBanana.setNum(10);
+ cart.getCartItems().add(cartItemBanana);
+
+ return cart;
+ }
+
+
+ /**
+ * on-exit 执行的方法不需要返回值
+ * @param shoppingCart
+ */
+ public void updateShoppingCart(ShoppingCart shoppingCart){
+ logger.trace("updateShoppingCart {}", shoppingCart);
+ if(logger.isTraceEnabled()){
+ for(ShoppingCart.CartItem item : shoppingCart.getCartItems()){
+ logger.trace(" item: {} {} , {} ", item.getProduct().getId(), item.getProduct().getName(), item.getNum());
+ }
+ }
+ }
+
+ /**
+ * action-state 中的 evaluate 执行的方法需要返回值;并且 action-state 中根据这个返回值来决定下一步去那里
+ * @param shoppingCart
+ * @param error
+ * @return
+ */
+ public String createOrder(ShoppingCart shoppingCart, Order order, MessageContext error) {
+ logger.trace("createOrder shoppingCart:{}, order: {}, eror: {}", shoppingCart, order, error);
+
+ String returnValue = "success";
+
+ if (shoppingCart.getCartItems() == null || shoppingCart.getCartItems().size() == 0) {
+ error.addMessage(new MessageBuilder(). //
+ error() //
+ .defaultText("购物车是空的") //
+ .build());
+
+ returnValue = "fail";
+ }
+
+ // 还应检查购物车内物品是否已经下架等
+
+ //创建订单
+ order.setId("O99999");
+ order.setPayDate(new Date());
+
+ return returnValue;
+ }
+
+ public String payOrder(Order order){
+ logger.trace("payOrder {}", order == null ? "null" : order.getId());
+
+ return "success";
+ }
+}
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/HomeController.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/HomeController.java
new file mode 100644
index 0000000..9b431c4
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/HomeController.java
@@ -0,0 +1,18 @@
+package cn.devmgr.tutorial.webflow;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class HomeController {
+ private final static Logger logger = LoggerFactory.getLogger(HomeController.class);
+
+
+ @GetMapping("/")
+ public String home(){
+ logger.trace("home()");
+ return "index";
+ }
+}
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/WebFlowConfig.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/WebFlowConfig.java
new file mode 100644
index 0000000..a8512ca
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/WebFlowConfig.java
@@ -0,0 +1,110 @@
+package cn.devmgr.tutorial.webflow;
+
+
+import java.util.Collections;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+import org.springframework.webflow.config.AbstractFlowConfiguration;
+import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
+import org.springframework.webflow.engine.builder.ViewFactoryCreator;
+import org.springframework.webflow.engine.builder.support.FlowBuilderServices;
+import org.springframework.webflow.executor.FlowExecutor;
+import org.springframework.webflow.mvc.builder.MvcViewFactoryCreator;
+import org.springframework.webflow.mvc.servlet.FlowHandlerAdapter;
+import org.springframework.webflow.mvc.servlet.FlowHandlerMapping;
+import org.thymeleaf.spring5.SpringTemplateEngine;
+import org.thymeleaf.spring5.webflow.view.AjaxThymeleafViewResolver;
+import org.thymeleaf.spring5.webflow.view.FlowAjaxThymeleafView;
+import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
+
+@Configuration
+public class WebFlowConfig extends AbstractFlowConfiguration {
+
+
+ @Autowired
+ private LocalValidatorFactoryBean localValidatorFacotryBean;
+
+ @Bean
+ public FlowDefinitionRegistry flowRegistry() {
+ return getFlowDefinitionRegistryBuilder() //
+ .setBasePath("classpath:flows") //
+ .addFlowLocationPattern("/**/*-flow.xml") //
+ .setFlowBuilderServices(this.flowBuilderServices()) //
+ .build();
+ }
+
+ @Bean
+ public FlowExecutor flowExecutor() {
+ return getFlowExecutorBuilder(this.flowRegistry()) //
+ .build();
+ }
+
+ @Bean
+ public FlowBuilderServices flowBuilderServices() {
+ return getFlowBuilderServicesBuilder() //
+ .setViewFactoryCreator(this.mvcViewFactoryCreator()) // Important!
+ .setValidator(this.localValidatorFacotryBean).build();
+ }
+
+ /**
+ * 用于设置 webflow 的 urlmapping
+ * @return
+ */
+ @Bean
+ public FlowHandlerMapping flowHandlerMapping() {
+ FlowHandlerMapping handlerMapping = new FlowHandlerMapping();
+ handlerMapping.setOrder(-1);
+ handlerMapping.setFlowRegistry(this.flowRegistry());
+ return handlerMapping;
+ }
+
+ @Bean
+ public FlowHandlerAdapter flowHandlerAdapter() {
+ FlowHandlerAdapter handlerAdapter = new FlowHandlerAdapter();
+ handlerAdapter.setFlowExecutor(this.flowExecutor());
+ handlerAdapter.setSaveOutputToFlashScopeOnRedirect(true);
+ return handlerAdapter;
+ }
+
+ @Bean
+ public ViewFactoryCreator mvcViewFactoryCreator() {
+ MvcViewFactoryCreator factoryCreator = new MvcViewFactoryCreator();
+ factoryCreator.setViewResolvers(Collections.singletonList(this.thymeleafViewResolver()));
+ factoryCreator.setUseSpringBeanBinding(true);
+ return factoryCreator;
+ }
+
+ @Bean
+ @Description("Thymeleaf AJAX view resolver for Spring WebFlow")
+ public AjaxThymeleafViewResolver thymeleafViewResolver() {
+ AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver();
+ viewResolver.setViewClass(FlowAjaxThymeleafView.class);
+ viewResolver.setTemplateEngine(this.templateEngine());
+ viewResolver.setCharacterEncoding("UTF-8");
+ return viewResolver;
+ }
+
+ @Bean
+ @Description("Thymeleaf template resolver serving HTML 5")
+ public ClassLoaderTemplateResolver templateResolver() {
+ ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
+ templateResolver.setPrefix("templates/");
+ templateResolver.setCacheable(false);
+ templateResolver.setSuffix(".html");
+ templateResolver.setTemplateMode("HTML5");
+ templateResolver.setCharacterEncoding("UTF-8");
+ return templateResolver;
+ }
+
+ @Bean
+ @Description("Thymeleaf template engine with Spring integration")
+ public SpringTemplateEngine templateEngine() {
+ SpringTemplateEngine templateEngine = new SpringTemplateEngine();
+ templateEngine.setTemplateResolver(this.templateResolver());
+ return templateEngine;
+ }
+}
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/Order.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/Order.java
new file mode 100644
index 0000000..2063641
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/Order.java
@@ -0,0 +1,33 @@
+package cn.devmgr.tutorial.webflow.pojo;
+
+import java.io.Serializable;
+import java.util.Date;
+
+public class Order implements Serializable {
+
+ private String id;
+ private Date payDate;
+
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+
+ public Date getPayDate() {
+ return payDate;
+ }
+
+ public void setPayDate(Date payDate) {
+ this.payDate = payDate;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getName() + "{id=" + id + "; payDate=" + payDate + "}";
+ }
+}
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/Product.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/Product.java
new file mode 100644
index 0000000..1b19341
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/Product.java
@@ -0,0 +1,24 @@
+package cn.devmgr.tutorial.webflow.pojo;
+
+import java.io.Serializable;
+
+public class Product implements Serializable {
+ private String id;
+ private String name;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/ShoppingCart.java b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/ShoppingCart.java
new file mode 100644
index 0000000..0c216ea
--- /dev/null
+++ b/section-18-web-flow/src/main/java/cn/devmgr/tutorial/webflow/pojo/ShoppingCart.java
@@ -0,0 +1,40 @@
+package cn.devmgr.tutorial.webflow.pojo;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShoppingCart implements Serializable {
+
+ private List cartItems = new ArrayList<>();
+
+ public List getCartItems() {
+ return cartItems;
+ }
+
+ public void setCartItems(List cartItems) {
+ this.cartItems = cartItems;
+ }
+
+ public class CartItem implements Serializable{
+ private Product product;
+ private int num;
+
+ public Product getProduct() {
+ return product;
+ }
+
+ public void setProduct(Product product) {
+ this.product = product;
+ }
+
+ public int getNum() {
+ return num;
+ }
+
+ public void setNum(int num) {
+ this.num = num;
+ }
+ }
+}
+
diff --git a/section-18-web-flow/src/main/resources/application.yml b/section-18-web-flow/src/main/resources/application.yml
new file mode 100644
index 0000000..4138a41
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/application.yml
@@ -0,0 +1,9 @@
+
+
+logging:
+ file: target/app.log
+ level:
+ ROOT: WARN
+ cn.devmgr: TRACE
+ org.springframework: INFO
+ org.thymeleaf.TemplateEngine: INFO
diff --git a/section-18-web-flow/src/main/resources/flows/buy/buy-flow.xml b/section-18-web-flow/src/main/resources/flows/buy/buy-flow.xml
new file mode 100644
index 0000000..a768e47
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/flows/buy/buy-flow.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/static/css/main.css b/section-18-web-flow/src/main/resources/static/css/main.css
new file mode 100644
index 0000000..dab2f09
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/static/css/main.css
@@ -0,0 +1,12 @@
+.linkbutton{display:inline-block; margin-right:50px;}
+
+.shoppingcartcontainer{width:400px;}
+.cartitem{display: grid; grid-template-columns: 300px 80px; border-bottom:1px solid #AAA; padding:10px 20px;}
+.shoppingcartcontainer .cartitem:first-child{border-top:1px solid #AAA;}
+.product{}
+.product .productid{display: block; color:#666;}
+.product .productname{display: block;}
+.cartitem .num{padding-left:20px;}
+.num input{width: 50px;}
+
+.buttons{margin-top:20px;}
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/templates/flows/buy/completion.html b/section-18-web-flow/src/main/resources/templates/flows/buy/completion.html
new file mode 100644
index 0000000..4e1543a
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/templates/flows/buy/completion.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
购物成功
+
+ 您的订单已经被接收。
+
+
order id: orderId
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/templates/flows/buy/confirmorder.html b/section-18-web-flow/src/main/resources/templates/flows/buy/confirmorder.html
new file mode 100644
index 0000000..29a609e
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/templates/flows/buy/confirmorder.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/templates/flows/buy/payorder.html b/section-18-web-flow/src/main/resources/templates/flows/buy/payorder.html
new file mode 100644
index 0000000..13b9862
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/templates/flows/buy/payorder.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
清付款
+ 您的订单已经被接收。
+
+
order id: orderId
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/templates/flows/buy/showcart.html b/section-18-web-flow/src/main/resources/templates/flows/buy/showcart.html
new file mode 100644
index 0000000..4379aba
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/templates/flows/buy/showcart.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
购物车
+
+
order id: orderId
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/templates/fragments/head.html b/section-18-web-flow/src/main/resources/templates/fragments/head.html
new file mode 100644
index 0000000..b0be619
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/templates/fragments/head.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/main/resources/templates/index.html b/section-18-web-flow/src/main/resources/templates/index.html
new file mode 100644
index 0000000..20af833
--- /dev/null
+++ b/section-18-web-flow/src/main/resources/templates/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/section-18-web-flow/src/test/java/cn/devmgr/tutorial/AppTest.java b/section-18-web-flow/src/test/java/cn/devmgr/tutorial/AppTest.java
new file mode 100644
index 0000000..82771c9
--- /dev/null
+++ b/section-18-web-flow/src/test/java/cn/devmgr/tutorial/AppTest.java
@@ -0,0 +1,17 @@
+package cn.devmgr.tutorial;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class AppTest {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/section-18-web-flow/src/test/resources/blank.txt b/section-18-web-flow/src/test/resources/blank.txt
new file mode 100644
index 0000000..e69de29
diff --git a/section-19-restdocs/README.md b/section-19-restdocs/README.md
new file mode 100644
index 0000000..e4fafba
--- /dev/null
+++ b/section-19-restdocs/README.md
@@ -0,0 +1,83 @@
+# REST Docs
+
+RESTDocs 是通过编写测试用例的方式,来制作文档。当测试用例都通过测试后,会按照测试用例的内容来生成adoc格式的文档。
+
+不需要对 API 做任何形式的修改,pom中增加的restdocs 的依赖scope都是test,也不会对生产环境产生影响。
+
+## pom
+
+### 增加依赖
+
+```xml
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+ junit
+ junit
+
+
+
+```
+
+排除掉spring-bootstarter-test中的junit相关依赖是因为他们的版本较低, restdocs 中包含版本更高的junit,restdocs也需要这些高版本的junit。
+
+
+### 增加插件
+
+```xml
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ 1.5.3
+
+
+ generate-docs
+ prepare-package
+
+ process-asciidoc
+
+
+ html
+ book
+
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-asciidoctor
+ ${spring-restdocs.version}
+
+
+
+```
+
+
+## 测试用例
+
+具体可参照 [src/test/java/cn/devmgr/tutorial/restdoc](src/test/java/cn/devmgr/tutorial/restdoc)目录下的测试用例。
+
+
+```java
+@AutoConfigureMockMvc //自动配置mockMvc,缺少这个注解会无法@Autowired MockMvc类
+@AutoConfigureRestDocs //自动配置RestDocs,缺少这个注解且没手工配置,会导致出现空指针异常
+```
+## 生成文档
+
+如果测试用例都写好后,运行 `mvn package` 如果测试用例都通过,会自动生成文档,文档在 target/generated-snippets 目录下。
+
+这个目录下的文档都是 Asciidoctor 格式的。
+
diff --git a/section-19-restdocs/pom.xml b/section-19-restdocs/pom.xml
new file mode 100644
index 0000000..d0848e0
--- /dev/null
+++ b/section-19-restdocs/pom.xml
@@ -0,0 +1,116 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.M3
+
+
+ cn.devmgr.tutorial.restdoc
+ section-19-restdocs
+ 0.0.1-SNAPSHOT
+ section-19-restdocs
+ Demo project for Spring Boot
+
+
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+ junit
+ junit
+
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+
+
+
+
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ 1.5.3
+
+
+ generate-docs
+ prepare-package
+
+ process-asciidoc
+
+
+ html
+ book
+
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-asciidoctor
+ ${spring-restdocs.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+
+
+
diff --git a/section-19-restdocs/src/main/java/cn/devmgr/tutorial/restdoc/Application.java b/section-19-restdocs/src/main/java/cn/devmgr/tutorial/restdoc/Application.java
new file mode 100644
index 0000000..a7d3f39
--- /dev/null
+++ b/section-19-restdocs/src/main/java/cn/devmgr/tutorial/restdoc/Application.java
@@ -0,0 +1,13 @@
+package cn.devmgr.tutorial.restdoc;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/section-19-restdocs/src/main/java/cn/devmgr/tutorial/restdoc/SampleController.java b/section-19-restdocs/src/main/java/cn/devmgr/tutorial/restdoc/SampleController.java
new file mode 100644
index 0000000..377b98f
--- /dev/null
+++ b/section-19-restdocs/src/main/java/cn/devmgr/tutorial/restdoc/SampleController.java
@@ -0,0 +1,22 @@
+package cn.devmgr.tutorial.restdoc;
+
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/")
+public class SampleController {
+
+
+ @GetMapping
+ public Map getAll(){
+ Map map = new HashMap<>();
+ map.put("message", "OK");
+ return map;
+ }
+}
diff --git a/section-19-restdocs/src/main/resources/application.yml b/section-19-restdocs/src/main/resources/application.yml
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/section-19-restdocs/src/main/resources/application.yml
@@ -0,0 +1 @@
+
diff --git a/section-19-restdocs/src/test/java/cn/devmgr/tutorial/restdoc/ApplicationTests.java b/section-19-restdocs/src/test/java/cn/devmgr/tutorial/restdoc/ApplicationTests.java
new file mode 100644
index 0000000..bb14f1a
--- /dev/null
+++ b/section-19-restdocs/src/test/java/cn/devmgr/tutorial/restdoc/ApplicationTests.java
@@ -0,0 +1,53 @@
+package cn.devmgr.tutorial.restdoc;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@AutoConfigureRestDocs
+class ApplicationTests {
+
+ @Autowired private MockMvc mockMvc;
+
+ @Autowired
+ private WebApplicationContext context;
+
+
+
+ @Test
+ public void contextLoads() throws Exception {
+
+ this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andDo(document("home",
+ responseFields(
+ fieldWithPath("message").description("返回的消息")
+ )
+ ));
+ }
+
+
+}
diff --git a/section-20-two-datasours/README.md b/section-20-two-datasours/README.md
new file mode 100644
index 0000000..0b69c03
--- /dev/null
+++ b/section-20-two-datasours/README.md
@@ -0,0 +1,9 @@
+README
+===========================
+
+使用2个数据源
+
+DatabaseConfiguration 配置多个DataSource, SqlSessionFactory, SqlSessionTemplate 等,
+并设置每个Mapper用不同的SqlSessionTemplate,进而每个sql在不同的数据库内执行。
+
+
diff --git a/section-20-two-datasours/pom.xml b/section-20-two-datasours/pom.xml
new file mode 100644
index 0000000..b9ced4a
--- /dev/null
+++ b/section-20-two-datasours/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ cn.devmgr.tutorial
+ tutorial-section05
+ 0.0.1-SNAPSHOT
+ jar
+
+ tutorial-20
+ 用Spring Boot编写RESTful API教程 20
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.2
+
+
+
+
+ UTF-8
+ UTF-8
+ 11
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 2.2.2
+
+
+ org.postgresql
+ postgresql
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/section-20-two-datasours/sql.sql b/section-20-two-datasours/sql.sql
new file mode 100644
index 0000000..65c9883
--- /dev/null
+++ b/section-20-two-datasours/sql.sql
@@ -0,0 +1,42 @@
+
+/**
+ * 测试@Transactional注解用
+ */
+
+create database db1;
+
+## 进入db1, 拷贝粘贴时需注意,下面这行要单独执行,和其他一起拷贝执行可能不成功
+\c db1
+
+select current_database();
+
+create table person(
+ id int primary key,
+ name varchar(50) not null,
+ attributes json not null,
+ birthday timestamp default now(),
+ post_code varchar(10) not null,
+ detail_address varchar(100) null
+);
+
+insert into person(id, name, attributes, birthday, post_code, detail_address) values (1, 'jack', '{}', '1999/01/01 13:13', '456', 'road 1');
+insert into person(id, name, attributes, birthday, post_code, detail_address) values (2, 'tom', '{}', '2000/01/01 13:13', '123', 'road 2');
+insert into person(id, name, attributes, birthday, post_code, detail_address) values (3, 'john', '{}', '2011/01/01 13:13', '789', 'road 3');
+
+create database db2;
+
+## 进入db2, 拷贝粘贴时需注意,下面这行要单独执行,和其他一起拷贝执行可能不成功
+\c db2
+
+select current_database();
+
+create table hero(
+ id int primary key,
+ name varchar(50) not null,
+ attributes json not null,
+ birthday timestamp default now()
+);
+
+insert into hero(id, name, attributes, birthday) values (101, 'Hero A', '{}', '1999/03/01 13:13');
+insert into hero(id, name, attributes, birthday) values (102, 'Hero B', '{}', '2000/03/01 13:13');
+insert into hero(id, name, attributes, birthday) values (103, 'Hero 3', '{}', '2011/03/01 13:13');
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/Application.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/Application.java
new file mode 100644
index 0000000..6b9800d
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/Application.java
@@ -0,0 +1,15 @@
+package cn.devmgr.tutorial;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+@MapperScan("cn.devmgr.tutorial.tx")
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/DatabaseConfiguration.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/DatabaseConfiguration.java
new file mode 100644
index 0000000..1a52520
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/DatabaseConfiguration.java
@@ -0,0 +1,132 @@
+package cn.devmgr.tutorial;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.mybatis.spring.annotation.MapperScan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+@Configuration
+@MapperScan(
+ annotationClass = SecondaryMapper.class,
+ basePackages = "cn.devmgr",
+ sqlSessionFactoryRef = "secondarySqlSessionFactory",
+ sqlSessionTemplateRef = "secondarySqlSessionTemplate")
+@MapperScan(
+ annotationClass = Mapper.class,
+ basePackages = "cn.devmgr",
+ sqlSessionFactoryRef = "mainSqlSessionFactory",
+ sqlSessionTemplateRef = "mainSqlSessionTemplate")
+public class DatabaseConfiguration {
+ private static final Logger logger = LoggerFactory.getLogger(DatabaseConfiguration.class);
+
+ // --------------------- main -----------------------------------------
+
+ @Bean(name = "mainDataSource")
+ @Primary
+ @ConfigurationProperties(prefix = "spring.datasource.main")
+ public DataSource getMainServiceDataSource() {
+ logger.trace("getMainServiceDataSource....");
+ return DataSourceBuilder.create().build();
+ }
+
+ @Bean(name = "mainSqlSessionFactory")
+ @Primary
+ public SqlSessionFactory mainSqlSessionFactory(@Qualifier("mainDataSource") DataSource dataSource)
+ throws Exception {
+ logger.trace("mainSqlSessionFactory....");
+ SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
+ bean.setDataSource(dataSource);
+ return bean.getObject();
+ }
+
+ // @Bean(name = "mainTransactionManager")
+ // @Primary
+ // public DataSourceTransactionManager mainTransactionManager(
+ // @Qualifier("mainDataSource") DataSource dataSource) {
+ // try {
+ // logger.trace("mainTransactionManager {}", dataSource.getConnection().isReadOnly());
+ // Connection conn = dataSource.getConnection();
+ // Statement statement = conn.createStatement();
+ // ResultSet rs = statement.executeQuery("select current_database()");
+ // while (rs.next()) {
+ // logger.trace("mainTransactionManager current database: {}", rs.getString(1));
+ // }
+ // rs.close();
+ // statement.close();
+ // } catch (Exception ex) {
+ // logger.error("ERROR", ex);
+ // }
+ // return new DataSourceTransactionManager(dataSource);
+ // }
+
+ @Bean(name = "mainSqlSessionTemplate")
+ @Primary
+ public SqlSessionTemplate mainSqlSessionTemplate(
+ @Qualifier("mainSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
+ logger.trace("mainSqlSessionFactory....");
+ return new SqlSessionTemplate(sqlSessionFactory);
+ }
+
+ // --------------------- secondary -------------------------------------
+ @Bean(name = "secondaryDataSource")
+ @ConfigurationProperties(prefix = "spring.datasource.secondary")
+ public DataSource getSecondaryServiceDataSource() {
+ logger.trace("getSecondaryServiceDataSource....");
+ return DataSourceBuilder.create().build();
+ }
+
+ @Bean(name = "secondarySqlSessionFactory")
+ public SqlSessionFactory secondarySqlSessionFactory(
+ @Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
+ logger.trace("secondarySqlSessionFactory....");
+ SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
+ bean.setDataSource(dataSource);
+ bean.setMapperLocations(
+ new PathMatchingResourcePatternResolver()
+ .getResources("classpath*:cn/devmgr/mall/order/SecondaryOrderDao.xml"));
+ return bean.getObject();
+ }
+
+ @Bean(name = "secondaryTransactionManager")
+ public DataSourceTransactionManager secondaryTransactionManager(
+ @Qualifier("secondaryDataSource") DataSource dataSource) {
+ try {
+ logger.trace("secondaryTransactionManager {}", dataSource.getConnection().isReadOnly());
+ Connection conn = dataSource.getConnection();
+ Statement statement = conn.createStatement();
+ ResultSet rs = statement.executeQuery("select current_database()");
+ while (rs.next()) {
+ logger.trace("current database: {}", rs.getString(1));
+ }
+ rs.close();
+ statement.close();
+ } catch (Exception ex) {
+ logger.error("ERROR", ex);
+ }
+ DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
+ return tm;
+ }
+
+ @Bean(name = "secondarySqlSessionTemplate")
+ public SqlSessionTemplate secondarySqlSessionTemplate(
+ @Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
+ logger.trace("secondarySqlSessionTemplate....");
+ return new SqlSessionTemplate(sqlSessionFactory);
+ }
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/JsonTypeHandler.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/JsonTypeHandler.java
new file mode 100644
index 0000000..f12910c
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/JsonTypeHandler.java
@@ -0,0 +1,106 @@
+package cn.devmgr.tutorial;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class JsonTypeHandler extends BaseTypeHandler {
+ private static final Logger logger = LoggerFactory.getLogger(JsonTypeHandler.class);
+ private static ObjectMapper objectMapper;
+
+ static {
+ objectMapper = new ObjectMapper();
+ }
+
+ private Class type;
+
+ public JsonTypeHandler(Class type) {
+ this.type = type;
+ if (logger.isTraceEnabled()) {
+ Type[] ts = type.getGenericInterfaces();
+ logger.trace(
+ " {}, {}, {}, getGenericInterfaces:{} getGenericSuperclass:{} ",
+ type,
+ type.getTypeParameters(),
+ type.isArray(),
+ type.getGenericInterfaces(),
+ type.getGenericSuperclass());
+ if (ts != null && ts.length > 0) {
+ if (type.getGenericInterfaces()[0] instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type.getGenericInterfaces()[0];
+ Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+ for (Type actualTypeArgument : actualTypeArguments) {
+ logger.trace("\tactualTypeArgument:{}", actualTypeArgument);
+ }
+ } else {
+ logger.trace("\t type.getGenericInterfaces()[0]: {}", type.getGenericInterfaces()[0]);
+ }
+ }
+ }
+ }
+
+ private T parse(String json) {
+ try {
+ if (json == null || json.length() == 0) {
+ return null;
+ }
+ // if (type.isAssignableFrom(List.class)) {
+ // logger.trace("try to read json using TypeReference");
+ TypeReference tr =
+ new TypeReference() {
+ @Override
+ public Type getType() {
+ return type;
+ }
+ };
+ return objectMapper.readValue(json, tr);
+ // }
+ // logger.trace("type not assignable from list");
+ // return objectMapper.readValue(json, type);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String toJsonString(Object obj) {
+ try {
+ return objectMapper.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
+ return (T) parse(rs.getString(columnName));
+ }
+
+ @Override
+ public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+ return (T) parse(rs.getString(columnIndex));
+ }
+
+ @Override
+ public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+ return (T) parse(cs.getString(columnIndex));
+ }
+
+ @Override
+ public void setNonNullParameter(
+ PreparedStatement ps, int columnIndex, Object parameter, JdbcType jdbcType)
+ throws SQLException {
+ ps.setString(columnIndex, toJsonString(parameter));
+ }
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/SecondaryMapper.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/SecondaryMapper.java
new file mode 100644
index 0000000..994ff7e
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/SecondaryMapper.java
@@ -0,0 +1,10 @@
+package cn.devmgr.tutorial;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SecondaryMapper {}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/Address.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/Address.java
new file mode 100644
index 0000000..8feb6ef
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/Address.java
@@ -0,0 +1,27 @@
+package cn.devmgr.tutorial.tx;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Address {
+ private static final Logger logger = LoggerFactory.getLogger(Address.class);
+
+ private String postCode;
+ private String detailAddress;
+
+ public String getPostCode() {
+ return postCode;
+ }
+
+ public void setPostCode(String postCode) {
+ this.postCode = postCode;
+ }
+
+ public String getDetailAddress() {
+ return detailAddress;
+ }
+
+ public void setDetailAddress(String detailAddress) {
+ this.detailAddress = detailAddress;
+ }
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/HeroDao.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/HeroDao.java
new file mode 100644
index 0000000..26490a3
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/HeroDao.java
@@ -0,0 +1,17 @@
+package cn.devmgr.tutorial.tx;
+
+import cn.devmgr.tutorial.SecondaryMapper;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.List;
+
+@SecondaryMapper
+public interface HeroDao {
+
+ @Select("select * from hero")
+ public List getAll();
+
+ @Update("update hero set name=#{name}, birthday=#{birthday} where id=#{id}")
+ public int update(HeroDto person);
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/HeroDto.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/HeroDto.java
new file mode 100644
index 0000000..523ceca
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/HeroDto.java
@@ -0,0 +1,48 @@
+package cn.devmgr.tutorial.tx;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+import java.util.Map;
+
+public class HeroDto {
+ private static final Logger logger = LoggerFactory.getLogger(HeroDto.class);
+
+ private int id;
+ private String name;
+ private Date birthday;
+ private Map attributes;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Date getBirthday() {
+ return birthday;
+ }
+
+ public void setBirthday(Date birthday) {
+ this.birthday = birthday;
+ }
+
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map attributes) {
+ this.attributes = attributes;
+ }
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonDao.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonDao.java
new file mode 100644
index 0000000..5652620
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonDao.java
@@ -0,0 +1,15 @@
+package cn.devmgr.tutorial.tx;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.List;
+
+@Mapper
+public interface PersonDao {
+
+ public List getAll();
+
+ @Update("update person set name=#{name}, birthday=#{birthday} where id=#{id}")
+ public int update(PersonDto person);
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonDto.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonDto.java
new file mode 100644
index 0000000..e14181f
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonDto.java
@@ -0,0 +1,65 @@
+package cn.devmgr.tutorial.tx;
+
+import java.util.Date;
+import java.util.Map;
+
+public class PersonDto {
+ private int id;
+ private String name;
+ private Date birthday;
+ private Map attributes;
+ private Address address;
+
+ public PersonDto() {}
+
+ public PersonDto(int id, String name) {
+ this.id = id;
+ this.name = name;
+ this.birthday = new Date();
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Date getBirthday() {
+ return birthday;
+ }
+
+ public void setBirthday(Date birthday) {
+ this.birthday = birthday;
+ }
+
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map attributes) {
+ this.attributes = attributes;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "{id=" + id + ", name=" + name + "}";
+ }
+}
diff --git a/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonService.java b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonService.java
new file mode 100644
index 0000000..081705d
--- /dev/null
+++ b/section-20-two-datasours/src/main/java/cn/devmgr/tutorial/tx/PersonService.java
@@ -0,0 +1,24 @@
+package cn.devmgr.tutorial.tx;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class PersonService {
+ private static final Logger logger = LoggerFactory.getLogger(PersonService.class);
+
+ @Autowired private PersonDao personDao;
+ @Autowired private HeroDao heroDao;
+
+ public List queryPersons() {
+ return personDao.getAll();
+ }
+
+ public List queryHeros() {
+ return heroDao.getAll();
+ }
+}
diff --git a/section-20-two-datasours/src/main/resources/application.yml b/section-20-two-datasours/src/main/resources/application.yml
new file mode 100644
index 0000000..9edb194
--- /dev/null
+++ b/section-20-two-datasours/src/main/resources/application.yml
@@ -0,0 +1,41 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: spring-boot-tutorial
+ datasource:
+ main:
+ driverClassName: org.postgresql.Driver
+ jdbc-url: jdbc:postgresql://127.0.0.1:5432/db1
+ username: pgdbo
+ password: sql
+ hikari:
+ maximum-pool-size: 30
+ minimum-idle: 5 # 空闲时保持最小连接数
+ idle-timeout: 10000 # 空闲连接存活时间
+ connection-timeout: 8000 # 连接超时时间
+ connection-test-query: select 2 # 测试sql
+ secondary:
+ driverClassName: org.postgresql.Driver
+ jdbc-url: jdbc:postgresql://127.0.0.1:5432/db2
+ username: pgdbo
+ password: sql
+ hikari:
+ maximum-pool-size: 10
+ minimum-idle: 3 # 空闲时保持最小连接数
+ idle-timeout: 10000 # 空闲连接存活时间
+ connection-timeout: 8000 # 连接超时时间
+ connection-test-query: select 4 # 测试sql
+ read-only: true #此项目将连接从数据库(只读数据库),所以此处设置成只读,以发现避免环境误写状况
+ jackson:
+ serialization:
+ write-dates-as-timestamps: true #使用数值timestamp表示日期
+
+mybatis.configuration.mapUnderscoreToCamelCase: true
+
+logging:
+ file: target/app.log
+ level:
+ ROOT: WARN
+ cn.devmgr: TRACE
diff --git a/section-20-two-datasours/src/main/resources/cn/devmgr/tutorial/tx/PersonDao.xml b/section-20-two-datasours/src/main/resources/cn/devmgr/tutorial/tx/PersonDao.xml
new file mode 100644
index 0000000..3d044ac
--- /dev/null
+++ b/section-20-two-datasours/src/main/resources/cn/devmgr/tutorial/tx/PersonDao.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select * from person
+
+
+
+
\ No newline at end of file
diff --git a/section-20-two-datasours/src/test/java/cn/devmgr/tutorial/tx/test/AppTests.java b/section-20-two-datasours/src/test/java/cn/devmgr/tutorial/tx/test/AppTests.java
new file mode 100644
index 0000000..e954771
--- /dev/null
+++ b/section-20-two-datasours/src/test/java/cn/devmgr/tutorial/tx/test/AppTests.java
@@ -0,0 +1,34 @@
+package cn.devmgr.tutorial.tx.test;
+
+import cn.devmgr.tutorial.tx.HeroDto;
+import cn.devmgr.tutorial.tx.PersonDto;
+import cn.devmgr.tutorial.tx.PersonService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.util.List;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+public class AppTests {
+ private static final Logger logger = LoggerFactory.getLogger(AppTests.class);
+
+ @Autowired PersonService serv;
+
+ @Test
+ public void testSomething() {
+ List persons = serv.queryPersons();
+ List heroes = serv.queryHeros();
+ for (PersonDto person : persons) {
+ logger.trace("person: {}, {}, {}", person.getId(), person.getName());
+ }
+ for (HeroDto hero : heroes) {
+ logger.trace("hero: {}, {}, {}", hero.getId(), hero.getName());
+ }
+ }
+}
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/DNSClient.java" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/DNSClient.java"
new file mode 100644
index 0000000..ca43cc3
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/DNSClient.java"
@@ -0,0 +1,177 @@
+import java.io.*;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class DNSClient {
+ private final static int TIME_OUT = 10000;
+ private final static int PORT = 53;
+ private final static int BUF_SIZE = 8192;
+ private String dnsServer;
+
+ public DNSClient(){
+ this("8.8.8.8");
+ }
+ public DNSClient(String dnsServer){
+ this.dnsServer = dnsServer;
+ }
+
+ private List query(String domainName)
+ throws SocketTimeoutException, IOException {
+ DatagramSocket socket = new DatagramSocket(0);
+ socket.setSoTimeout(TIME_OUT);
+
+ ByteArrayOutputStream outBuf = new ByteArrayOutputStream(BUF_SIZE);
+ DataOutputStream output = new DataOutputStream(outBuf);
+ encodeDNSMessage(output, domainName);
+
+ InetAddress host = InetAddress.getByName(dnsServer);
+ DatagramPacket request = new DatagramPacket(outBuf.toByteArray(), outBuf.size(), host, PORT);
+
+ socket.send(request);
+
+ byte[] inBuf = new byte[BUF_SIZE];
+ ByteArrayInputStream inBufArray = new ByteArrayInputStream(inBuf);
+ DataInputStream input = new DataInputStream(inBufArray);
+ DatagramPacket response = new DatagramPacket(inBuf, inBuf.length);
+
+ socket.receive(response);
+
+ List ipList = decodeDNSMessage(input);
+
+ socket.close();
+
+ return ipList;
+ }
+
+ private void encodeDNSMessage(DataOutputStream output, String domainName)
+ throws IOException {
+ // transaction id
+ output.writeShort(1);
+ // flags
+ output.writeShort(0x100);
+ // number of queries
+ output.writeShort(1);
+ // answer, auth, other
+ output.writeShort(0);
+ output.writeShort(0);
+ output.writeShort(0);
+
+ encodeDomainName(output, domainName);
+
+ // query type
+ output.writeShort(1);
+ // query class
+ output.writeShort(1);
+
+ output.flush();
+ }
+
+ private void encodeDomainName(DataOutputStream output, String domainName)
+ throws IOException {
+ //StringUtils.split(domainName, '.')
+ String[] ary = domainName.split("[.]");
+ for(String label : ary) {
+ output.writeByte((byte)label.length());
+ output.write(label.getBytes());
+ }
+ output.writeByte(0);
+ }
+
+ private List decodeDNSMessage(DataInputStream input)
+ throws IOException {
+ List ipList = new ArrayList<>();
+ // header
+ // transaction id
+ input.skip(2);
+ // flags
+ input.skip(2);
+ // number of queries
+ input.skip(2);
+ // answer, auth, other
+ short numberOfAnswer = input.readShort();
+ input.skip(2);
+ input.skip(2);
+
+ // question record
+ skipDomainName(input);
+ // query type
+ input.skip(2);
+ // query class
+ input.skip(2);
+
+ // answer records
+ for (int i = 0; i < numberOfAnswer; i++) {
+ input.mark(1);
+ byte ahead = input.readByte();
+ input.reset();
+ if ((ahead & 0xc0) == 0xc0) {
+ // compressed name
+ input.skip(2);
+ } else {
+ skipDomainName(input);
+ }
+
+ // query type
+ short type = input.readShort();
+ // query class
+ input.skip(2);
+ // ttl
+ input.skip(4);
+ short addrLen = input.readShort();
+ if (type == 1 && addrLen == 4) {
+ int addr = input.readInt();
+ ipList.add(longToIp(addr));
+ } else {
+ input.skip(addrLen);
+ }
+ }
+ return ipList;
+ }
+
+ private void skipDomainName(DataInputStream input) throws IOException {
+ byte labelLength = 0;
+ do {
+ labelLength = input.readByte();
+ input.skip(labelLength);
+ } while (labelLength != 0);
+ }
+
+ private String longToIp(long ip) {
+ return ((ip >> 24) & 0xFF) + "." + ((ip >> 16) & 0xFF) + "."
+ + ((ip >> 8) & 0xFF) + "." + (ip & 0xFF);
+ }
+
+
+ public static void main(String[] args) {
+ DNSClient dnsCLient;
+ String domainName = "devmgr.cn";
+ if(args.length == 2){
+ dnsCLient = new DNSClient(args[0]);
+ domainName = args[1];
+ }else if(args.length == 1){
+ domainName = args[0];
+ dnsCLient = new DNSClient();
+ }else{
+ dnsCLient = new DNSClient();
+ }
+ try {
+ List ipList = dnsCLient.query(domainName);
+ System.out.println(domainName + ":");
+ for (String ip : ipList) {
+ System.out.println("\t" + ip);
+ }
+ } catch (SocketTimeoutException ex) {
+ System.out.println("Timeout");
+ } catch (IOException ex) {
+ System.out.println("Unexpected IOException: " + ex);
+ ex.printStackTrace(System.out);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/HttpClient.java" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/HttpClient.java"
new file mode 100644
index 0000000..1d75054
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/HttpClient.java"
@@ -0,0 +1,42 @@
+import java.net.*;
+import java.io.*;
+
+public class HttpClient {
+
+ public static void main(String[] args) throws Exception {
+
+ String hostname = "www.baidu.com";
+ int port = 80;
+
+ try (Socket socket = new Socket(hostname, port)) {
+
+ OutputStream output = socket.getOutputStream();
+ PrintWriter writer = new PrintWriter(output, true);
+
+ writer.print("GET / HTTP/1.1\r\n");
+ writer.print("Host: " + hostname + ":" + port + "\r\n");
+ writer.print("User-Agent: MSIE\r\n");
+ writer.print("Accept: text/html\r\n");
+ writer.print("Accept-Language: en-US\r\n");
+ writer.print("Connection: close\r\n");
+ writer.print("\r\n");
+
+ InputStream input = socket.getInputStream();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ System.out.println(line);
+ }
+ writer.close();
+ output.close();
+ socket.close();
+ } catch (UnknownHostException ex) {
+ System.out.println("Server not found: " + ex.getMessage());
+ } catch (IOException ex) {
+ System.out.println("I/O error: " + ex.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/HttpServer.java" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/HttpServer.java"
new file mode 100644
index 0000000..fbf2807
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/HttpServer.java"
@@ -0,0 +1,57 @@
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Date;
+
+public class HttpServer {
+
+ public static void main(String[] argv) throws Exception {
+ int port = 8090;
+ ServerSocket serverSocket = new ServerSocket(port);
+
+ while (true) {
+ System.err.println("等待客户端接入... ...");
+ Socket client = serverSocket.accept();
+ System.err.println("有客户端接入 " + client.getInetAddress().getHostAddress());
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
+ PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
+
+ String action = in.readLine();
+ if(action == null) {
+ break;
+ }
+ System.out.println("客户端action: " + action);
+ StringBuffer clientHeaders = new StringBuffer();
+ for (String s = in.readLine(); s != null && s.length() > 0; s = in.readLine()) {
+ clientHeaders.append(s);
+ clientHeaders.append("\r\n");
+ }
+ System.out.println("客户端发送的Http Headers:\r\n" + clientHeaders);
+ if (action.startsWith("GET ")) {
+ // ok
+ String html = "\r\n" + "hello,world "
+ + "Hello, World! ";
+ out.print("HTTP/1.1 200 OK\r\n");
+ // out.print("Date:" + new Date().toGMTString() + "\r\n");
+ out.print("Context-Type: text/html\r\n");
+ out.print("Content-Length:" + html.length() + "\r\n");
+ out.print("Connection:close\r\n");
+ out.print("\r\n");
+ out.print(html);
+ } else {
+ out.print("HTTP/1.1 405 METHOD NOT ALLOWED\r\n");
+ out.print("Connection:close\r\n");
+ out.print("\r\n");
+ out.print("方法 (" + action + ") 不允许。\r\n");
+ }
+ out.close();
+ in.close();
+ client.close();
+ }
+ }
+}
\ No newline at end of file
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/README.md" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/README.md"
new file mode 100644
index 0000000..67d7122
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/README.md"
@@ -0,0 +1,4 @@
+此目录是《面向程序员的HTTP及相关协议》中用到的PPT和例子程序。
+
+https://study.163.com/course/courseMain.htm?courseId=1006121037
+
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/SQL\346\263\250\345\205\245.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/SQL\346\263\250\345\205\245.pdf"
new file mode 100644
index 0000000..00791c8
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/SQL\346\263\250\345\205\245.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/TcpClient.java" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/TcpClient.java"
new file mode 100644
index 0000000..f5c0c2c
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/TcpClient.java"
@@ -0,0 +1,32 @@
+import java.io.*;
+import java.net.*;
+
+
+/**
+ * TCP客户端的例子,运行前需要先启动TcpServer。
+ * 可以命令行参数传递一个字符串,如果没有命令行参数,则用"hello, tcpserver."代替。
+ * 把字符串发送给TcpServer,并显示TcpServer的返回值
+ */
+public class TcpClient {
+
+ public static void main(String argv[]) throws Exception {
+ String sentence;
+ String responseString;
+
+ Socket clientSocket = new Socket("localhost", 6789);
+ DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream());
+ BufferedReader inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+
+ if(argv.length > 0){
+ sentence = argv[0];
+ }else{
+ sentence = "hello, tcpserver.";
+ }
+ outToServer.writeBytes(sentence + '\n');
+ responseString = inFromServer.readLine();
+ System.out.println("Send to SERVER: " + sentence);
+ System.out.println("Received: " + responseString);
+ clientSocket.close();
+ }
+
+}
\ No newline at end of file
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/TcpServer.java" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/TcpServer.java"
new file mode 100644
index 0000000..093695b
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/TcpServer.java"
@@ -0,0 +1,28 @@
+import java.io.*;
+import java.net.*;
+
+/**
+ * 一个TCP Server的例子,收到字符串后,把字符串变成大写并返回给客户端
+ */
+public class TcpServer {
+
+ public static void main(String argv[]) throws Exception {
+ ServerSocket welcomeSocket = new ServerSocket(6789);
+ System.out.println("Server started, waiting for connections on 6789.");
+ while (true) {
+ System.out.println("waiting...");
+ Socket connectionSocket = welcomeSocket.accept();
+ System.out.println("One client connected.");
+ BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
+ DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
+ String clientSentence = inFromClient.readLine();
+ System.out.println("Received: " + clientSentence);
+ String capitalizedSentence = clientSentence.toUpperCase() + '\n';
+ outToClient.writeBytes(capitalizedSentence);
+ outToClient.close();
+ System.out.println("close client connection.");
+ connectionSocket.close();
+ }
+ }
+
+}
\ No newline at end of file
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/WebsocketClient.java" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/WebsocketClient.java"
new file mode 100644
index 0000000..1a77e4c
--- /dev/null
+++ "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/WebsocketClient.java"
@@ -0,0 +1,57 @@
+import java.net.*;
+import java.io.*;
+
+public class WebsocketClient {
+
+ public static void main(String[] args) throws Exception {
+
+ String hostname = "localhost";
+ int port = 8080;
+
+ try (Socket socket = new Socket(hostname, port)) {
+
+ OutputStream output = socket.getOutputStream();
+ PrintWriter writer = new PrintWriter(output, true);
+
+ writer.println("GET /chat HTTP/1.1");
+ writer.println("Host: " + hostname + ":" + port);
+ // 下面两行是告诉服务器端,此次连接的使用的http协议期望升级到websocket协议,如果服务端同意升级会返回
+ // Connection: upgrade upgrade: websocket表示已经升级了,而且状态吗是101 switching protocols
+ writer.println("Connection: upgrade");
+ writer.println("Upgrade: WebSocket");
+ // 下面这2行是必须的,sec-websocket-key是base64格式的一个随机数,由客户端生成
+ // sec-websocket-version是指明客户端的websocket版本
+ writer.println("Sec-WebSocket-Key: AQIDBAUGBwgJCgsMDQ4PEC==");
+ writer.println("Sec-WebSocket-Version: 13");
+ writer.println("User-Agent: MSIE");
+ writer.println("Origin: http://localhost:8080/");
+ writer.println();
+
+ InputStream input = socket.getInputStream();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+
+ System.out.println("request sent...");
+ int i=0;
+ while (i < 10) {
+ if (reader.ready()){
+ // 在读websocket发送过来的消息时,这里没有解码消息帧,都当字符串了,消息的数据报文格式前面有些事数据包控制的bit,所以打印时有乱码
+ char[] bin = new char[2048];
+ int len = reader.read(bin, 0, bin.length);
+ char[] buf = new char[len];
+ System.arraycopy(bin, 0, buf, 0, len);
+ System.out.println(new String(buf));
+ i++;
+ }
+ }
+ // 连接好了后可以发送数据,发送的数据报格式是 Base Framing Protocol, ABNF RFC5234
+ writer.close();
+ output.close();
+ socket.close();
+ } catch (UnknownHostException ex) {
+ System.out.println("Server not found: " + ex.getMessage());
+ } catch (IOException ex) {
+ System.out.println("I/O error: " + ex.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/XSS.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/XSS.pdf"
new file mode 100644
index 0000000..c45970e
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/XSS.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/csrf.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/csrf.pdf"
new file mode 100644
index 0000000..06395b6
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/csrf.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.1.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.1.pdf"
new file mode 100644
index 0000000..41f1d84
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.1.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.2.request.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.2.request.pdf"
new file mode 100644
index 0000000..40c7306
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.2.request.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.3.response.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.3.response.pdf"
new file mode 100644
index 0000000..57f71c6
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.3.response.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.4.cors.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.4.cors.pdf"
new file mode 100644
index 0000000..90e2281
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/http.4.cors.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.1.\346\246\202\350\277\260.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.1.\346\246\202\350\277\260.pdf"
new file mode 100644
index 0000000..5375927
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.1.\346\246\202\350\277\260.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.2.\350\257\201\344\271\246.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.2.\350\257\201\344\271\246.pdf"
new file mode 100644
index 0000000..43e22f5
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.2.\350\257\201\344\271\246.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.3.\345\256\242\346\210\267\347\253\257\350\256\244\350\257\201.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.3.\345\256\242\346\210\267\347\253\257\350\256\244\350\257\201.pdf"
new file mode 100644
index 0000000..a0a55ef
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/https.3.\345\256\242\346\210\267\347\253\257\350\256\244\350\257\201.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.2.nat-ipv6.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.2.nat-ipv6.pdf"
new file mode 100644
index 0000000..f47bdff
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.2.nat-ipv6.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.2.\344\270\211\346\254\241\346\217\241\346\211\213\345\217\212\344\276\213\345\255\220.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.2.\344\270\211\346\254\241\346\217\241\346\211\213\345\217\212\344\276\213\345\255\220.pdf"
new file mode 100644
index 0000000..c390747
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.2.\344\270\211\346\254\241\346\217\241\346\211\213\345\217\212\344\276\213\345\255\220.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.pdf"
new file mode 100644
index 0000000..4c1f5c5
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/tcpip.pdf" differ
diff --git "a/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/\345\216\237\345\210\231.pdf" "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/\345\216\237\345\210\231.pdf"
new file mode 100644
index 0000000..0ce5c79
Binary files /dev/null and "b/\350\265\204\346\226\231/\351\235\242\345\220\221\347\250\213\345\272\217\345\221\230\347\232\204HTTP\345\217\212\347\233\270\345\205\263\347\275\221\347\273\234\345\215\217\350\256\256/\345\216\237\345\210\231.pdf" differ