SpringBoot 入门终极篇(三)

到目前为止,这篇就是关于SpringBoot入门学习的最后一篇文章了,学完这三篇文章对 SpringBoot 也就有了一个初步的了解。

「 前言 」

本文的主要内容:

  • 事务处理
  • Docker安装及常用命令
  • 接入Redis缓存及配置Session
  • 整合MongoDB
  • 配置开发与生产环境
  • 部署项目到Docker上

「 事务处理 」

关于事务,可以简单理解为,当执行多条数据操作时,能确保每条操作能同时执行成功,否则有一条失败就会回滚前面所有执行成功的操作,保证一致性。下面我们来做一个简单的例子。

@Service
public class MyUserServices {

    @Resource
    private MyUserMapper userMapper;

    @Transactional(rollbackFor = IllegalArgumentException.class, noRollbackFor = IllegalStateException.class)
    public MyUser insertUser(MyUser user) {

        userMapper.insertUser(user);

        if (userMapper.selectUserByName2(user.getUserName()).size() >= 2) {
            throw new IllegalArgumentException("userName " +user.getUserName() + " is all through exist");
        }

        if (user.getPassword().equals("123456")) {
            throw new IllegalStateException("can't insert password 123456");
        }
        return user;
    }
}

可以看到我们给inserUser方法添加了Transactional注解,里面包含了rollbackFornoRollbackFor两个属性,通过字面上我们可以看出,一个是回滚一个是不回滚,value值都是接收异常class。可以这样理解,当方法中抛出rollbackFor定义的异常则会执行事务回滚,当方法中抛出noRollbackFor定义的异常则不会执行事务回滚。后面有两个判断,一个是通过判断如果插入了两个相同的用户名,则抛出异常进行回滚,实际上开发中不会这样去做,这里是为了学习事务处理来做的,另一个是通过判断密码为123456抛出异常但不进行回滚。

「 Docker安装及常用命令 」

Docker是一个开源的应用容器引擎,基于Go语言并遵从Apache2.0协议开源。可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似iPhone的app),更重要的是容器性能开销极低。

Docker下载地址:
Mac下载:https://download.docker.com/mac/stable/Docker.dmg
Windows下载:https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exe

安装完之后,运行下面命令,验证是否安装成功

docker version
# 或者
docker info

常用的docker命令如下:

# 搜索image
docker search [imageNmae]
# 拉取 image
docker pull [imageName]
# 列出本机所有 image 
docker image ls 
docker images
# 查看 image 信息
docker images [imageName]
# 强制删除 image 
docker rmi -f [imageId]
# 后台运行容器
docker run -d [imageName]
# 查看正在运行的容器
docker ps
# 杀掉容器
docker kill [containerId]
# 在运行的容器中执行命令
docker exec -it [containerId] [cmd]
# 强制删除容器
docker rm -f [containerId]

「 接入Redis缓存及配置Session 」

通过Docker添加Redis, 两行命令完成, pull命令时间可能会比较久,需要耐心等待。

# 拉取redis镜像文件
docker pull redis
# 运行redis容器
# -p 6379:6379: 将容器的6379端口映射到主机的6379端口
# -v $PWD/data:/data: 将主机中当前目录下的data挂载到容器的/data
# -d redis redis-server --appendonly yes: 在容器中后台执行redis-server启动命令,并打开redis持久化配置
docker run -p 6379:6379 -v $PWD/data:/data -d redis redis-server --appendonly yes

接下来添加Redis依赖

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

配置application.yml, 添加spring cache和redis配置, 密码默认为空:

spring:
  cache:
    type: redis
    cache-names: soaic
  redis:
    database: 0
    host: 192.168.0.184
    port: 6379
    password:
    jedis:
      pool:
        #连接池支持的最大连接数
        max-active: 1000
        #连接池中连接用完时,新的请求等待时间,毫秒
        max-wait: -1ms
        #连接池中最多可空闲maxIdle个连接
        max-idle: 400
        min-idle: 0
    timeout: 1000ms

MyUserServices核心代码:

@Service
public class MyUserServices {

    @Resource
    private MyUserMapper userMapper;

    @CachePut(value = "user", key = "#user.id")  //如果方法参数为对象,并且不指定key,需要重写toString方法
    @Transactional(rollbackFor = IllegalArgumentException.class, noRollbackFor = IllegalStateException.class)
    public MyUser insertUser(MyUser user) {

        userMapper.insertUser(user);
        System.out.println("添加缓存key为"+user.getId());

        if (userMapper.selectUserByName2(user.getUserName()).size() >= 2) {
            throw new IllegalArgumentException("userName " +user.getUserName() + " is all through exist");
        }

        if (user.getPassword().equals("123456")) {
            throw new IllegalStateException("can't insert password 123456");
        }
        return user;
    }

    @Cacheable(value = "user", key="id", condition="#id>0") //不指定key,默认以方法参数为key
    public MyUser selectUser(Integer id) {
        MyUser user = userMapper.selectUser(id);
        System.out.println("添加缓存key为" + id);
        return user;
    }

    @CacheEvict(value = "user")
    public Integer removeUser(Integer id) {
        Integer result = userMapper.deleteUser(id);
        System.out.println("删除缓存key为" + id);
        return result;
    }
}

注解@CachePut()表示当有缓存刷新缓存,没有则添加缓存,有三个属性,value为缓存的名称为application.yml配置的cache-names;key为缓存的key, 如果不指定key,默认以方法参数为key,如果方法参数为对象,需要重写toString方法;condition为缓存的条件,条件为true时才进行缓存。
注解@Cacheable()表示当有缓存则取缓存,没有则添加缓存,三个属性和注解@CachePut()属性一致。
注解@CacheEvict()表示当有缓存则清除缓存,除了前面说的三个属性外,还多出两个属性,allEntries为true清除所有元素;beforeInvocation为true时表示在调用该方法之前清除缓存中的指定元素,默认是方法成功执行之后触发,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。

接着添加RedisCacheConfig配置key生成规则、配置RedisTemplate和缓存序列化以json格式存储

@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
    /**
     * 配置redis key
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            for (Object obj : params) {
                sb.append(":");
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

    /**
     * RedisTemplate 序列化
     */
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        redisSerializer.setObjectMapper(objectMapper);

        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setValueSerializer(redisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    /**
     * redis 缓存序列化
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory){
        Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        redisSerializer.setObjectMapper(objectMapper);

        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));

        return RedisCacheManager.builder(connectionFactory).cacheDefaults(cacheConfiguration).build();
    }
}

最后把MyUser实现Serializable, 否则会报序列化错误

public class MyUser implements Serializable {
    ...
}

测试。先通过Controller(这里没有写出代码,可以到github上查看)调用MyUserServices中方法,然后进入docker运行的容器查看

# 查看正在运行的容器ID
docker ps
# 进入redis容器中
docker exec -it [containerId] redis-cli
# 进入之后输入 info 可以查看redis信息
info
# 查看所有key
keys *
# 查看某个key的值
get key

接下来就是配置Session了。为什么要用redis配置session?有这么几点原因:
1、方便管理session
2、托管到redis共享Session方便使用集群, 即一台服务器坏了,切换到另一台服务器,无需再次登录还能正常访问

首先添加依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

要注意一下的是,如果spring-boot-starter-parent版本是2.1.5.RELEASE时,添加依赖后运行会报下面这个错误,解决办法可以把版本降低,改成2.1.4.RELEASE即可。

java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration.taskScheduler

配置application.yml, 设置session存储方式

spring:
  session:
    store-type: redis

添加RedisSessionConfig配置,启用Session, 并设置失效时间

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)//原 Spring Boot 的 server.session.timeout 属性不再生效。
public class RedisSessionConfig {

}

接下来配置Session拦截器以及Login接口中登录成功设置session,上篇文章中介绍的还比较详细,这里就简单的贴一下核心代码:

SessionInterceptor核心代码

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("SessionInterceptor preHandle");
    HttpSession session = request.getSession(false);
    if (session != null && session.getAttribute("user") != null) {
        return true;
    } else {
        PrintWriter printWriter = response.getWriter();
        printWriter.write("{code: 501, message:\"not login!\"}");
        return false;
    }
}

MyUserController核心代码

@RequestMapping(value = "/login", method = RequestMethod.GET)
public ResponseResult<MyUser> login(HttpServletRequest request, String userName, String password) {
    ResponseResult<MyUser> responseResult;
    try {
        List<MyUser> myUser = myUserServices.login(userName, password);
        if (myUser != null && myUser.size() > 0) {
            request.getSession(true).setAttribute("user", myUser.get(0));
            responseResult = new ResponseResult<>(200, "login success", myUser.get(0));
        } else {
            responseResult = new ResponseResult<>(501, "login failure: invalid userName or password", null);
        }
    } catch (Exception e) {
        e.printStackTrace();
        responseResult = new ResponseResult<>(501, "login failure: " + e.getMessage(), null);
    }
    return responseResult;
}

最后测试,当第一时间访问其它接口会报501错误,而当访问login登录成功后,则会保存session,后面其它接口访问都可以正常。这里没有用多台服务器测试,只测试了下,当服务器停止再启动后,访问接口都不用再重新登录,session存储在redis中,不会因为服务器断开而消失,这就区别于不托管redis,session是存储在内存中,每次停止服务器再启动都要重新登录。

「 整合MongoDB 」

通过Docker添加MongoDB, 同样两行命令完成。

docker pull mongo
# -p 27017:27017: 将容器的27017 端口映射到主机的27017 端口
# -v $PWD/db:/data/db: 将主机中当前目录下的db挂载到容器的/data/db,作为mongo数据存储目录
docker run -p 27017:27017 -v $PWD/db:/data/db -d mongo

添加MongoDB依赖

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

配置application.yml, 这里用Docker添加的所以没有账号密码,当有账号密码时,url格式为:mongodb://user:pwd@ip:port/soaic?maxPoolSize=256; 当有多台数据库时,url格式为:mongodb://user:pwd@ip1:port1,ip2:port2/database?maxPoolSize=512

spring:
    data:
        mongodb:
          uri: mongodb://localhost:27017/soaic?maxPoolSize=256

修改MyUser对象, 在类作用域上添加@Document注解定义为一个文档,,也可以理解为是一张表,注解@Id定义属性为ID, 注解@Field定义为存储表中的字段名

@Document("myUser")
public class MyUser implements Serializable {

    @Id
    private String id;
    private String userName;
    private String password;

    @Field("roles")    //在文档中的名称为roles, 以数组形式存储
    private Collection<Role> roles = new LinkedHashSet<>();

}

添加 Role 对象

public class Role {

    private String roleName;

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }
}

在SpringBoot中对mongoDB操作数据有多种方式,如:通过继承MongoRepository对象调用定义好的方法, 或者遵从定义方法名的规则,或者通过注解@Query实现查询,?0为占位符取方法参数的第一个,依次类推

public interface MyUserRepository extends MongoRepository<MyUser, String> {

    List<MyUser> findByUserName(String name);

    @Query("{'userName': ?0}")
    List<MyUser> withQueryUserName(String name);

}

通过MongoTemplate来实现, 下面代码简单的实现了增删改查CRUD

@Service
public class MyUserDaoImpl implements MyUserDAO{

    @Autowired
    MongoTemplate mongoTemplate;

    @Override
    public List<MyUser> findByUserName(String userName) {
        Query query = new Query(Criteria.where("userName").is(userName));
        return mongoTemplate.find(query, MyUser.class);
    }

    @Override
    public MyUser insertUser(MyUser myUser) {
        return mongoTemplate.insert(myUser);
    }

    @Override
    public boolean deleteUser(String id) {
        Query query = new Query(Criteria.where("id").is(id));
        DeleteResult result = mongoTemplate.remove(query, MyUser.class);
        return result.getDeletedCount() > 0;
    }

    @Override
    public boolean updateUser(MyUser myUser) {
        Criteria criteria= Criteria.where("id").is(myUser.getId());
        Update update = new Update();
        if (myUser.getUserName() != null)
            update.set("userName", myUser.getUserName());
        if (myUser.getPassword() != null)
            update.set("password", myUser.getPassword());
        UpdateResult result = mongoTemplate.updateFirst(new Query(criteria), update, MyUser.class);
        return result.getModifiedCount() > 0;
    }
}

通过单元测试对上面的代码进行测试, 在test/java/com.soaic.hellospringboot中创建MongoDBTests, 添加注解@SpringBootTest@RunWith(SpringRunner.class), 测试的方法添加@Test注解,然后分别点击方法左边的绿色小图标 Run Test 即可

@RunWith(SpringRunner.class)
@SpringBootTest
public class MongoDBTests {

    @Autowired
    private MyUserRepository myUserRepository;

    @Autowired
    private MyUserDaoImpl myUserDaoImpl;

    @Test
    public void testSave() {
        MyUser myUser = new MyUser();
        myUser.setUserName("Soaic");
        myUser.setPassword("123456");

        Collection<Role> roles = new LinkedHashSet<>();
        Role role = new Role();
        role.setRoleName("管理员");
        roles.add(role);

        Role role1 = new Role();
        role1.setRoleName("程序员");
        roles.add(role1);
        myUser.setRoles(roles);

        //myUserRepository.save(myUser);
        myUserDaoImpl.insertUser(myUser);
    }

    @Test
    public void testFind() {
        List<MyUser> myUserList = myUserDaoImpl.findByUserName("Soaic");
        System.out.println(JSON.toJSONString(myUserList));
    }

    @Test
    public void testUpdate() {
        List<MyUser> myUserList = myUserRepository.withQueryUserName("Soaic");
        for (MyUser myUser: myUserList) {
            myUser.setPassword("1234567");
            myUserDaoImpl.updateUser(myUser);
        }
    }

    @Test
    public void testDel() {
        List<MyUser> myUserList = myUserRepository.findByUserName("Soaic");
        for (MyUser myUser: myUserList) {
            myUserDaoImpl.deleteUser(myUser.getId());
        }
    }

}

查询mongoDB数据库数据有无变化,可以通过如下命令连接mongoDB查询

# 连接mongodb
docker run -it mongo mongo --host 172.17.0.1
# 查看所有数据库
show dbs
# 切换数据库
use soaic
# 查看数据库状态
db.stats()
# 查询数据库下所有表
show collections
# 查询某个表的数据
db.collection.find() 

「 配置开发与生产环境 」

添加application-dev.yml开发环境配置和application-prod.yml生产环境配置, 如果我们想使用生产环境,因为程序会默认加载application.yml, 所以只需要在里面配置spring.profiles.activeprod即可,配置为dev则为开发环境。

spring:
  profiles:
    active: prod

「 部署项目到Docker上 」

部署到Docker上需要先把项目打包成jar包,可以通过idea中的 Maven Projects 找到Lifecycle下的clean 和 package 依次双击执行(也可以执行命令mvn clean package),最后就可以在target文件夹下找到hellospringboot-0.0.1-SNAPSHOT.jar

如果不部署到Docker上,可以直接运行下面命令启动项目

java -jar hellospringboot-0.0.1-SNAPSHOT.jar

如果部署到docker上,我们需要创建一个Dockerfile文件在项目的根目录,里面内容如下:

FROM java:8

MAINTAINER Soaic

ADD target/hellospringboot-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8443
EXPOSE 8088

ENTRYPOINT ["java", "-jar", "/app.jar"]

第一行:基于镜像为Java, 标签版本为8
第二行:作者Soaic
第三行:将hellospringboot-0.0.1-SNAPSHOT.jar添加到镜像中,并重命名为app.jar
第四行和第五行:运行镜像的容器,监听8443和8088端口
第六行:启动时运行 java -jar app.jar

接下来编译镜像,在项目根目录下执行下面命令,其中hellospringboot为镜像名称,最后一个".",用来指明Dockerfile路径,表示在当前路路径下,编译第一次需要下载java8,后面编译就不需要下载了

docker build -t hellospringboot .

编译完成后,可以通过下面命令查看及运行项目

# 查看是否有一个image为hellospringboot
docker images
# 后台运行项目,并映射两个端口号8443和8088,--name为修改运行容器名称可加可不加默认为image名称
docker run -d --name hellospringboot -p 8443:8443 -p 8088:8088 hellospringboot

最后可以通过 http://localhost:8088https://localhost:8443 这个两个地址访问了。

本文所有源码都已放在github上:https://github.com/soaic/HelloSpringBoot

以上就是这篇文章的全部内容,希望对大家能有所帮助,如有疑问或建议欢迎大家留言交流~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容