架构理念:[简单][高效][可依赖] 管理理念:[价值][勇气][专注]

trackr: An AngularJS app with a Java 8 backend – Part I

该系列文章来自techdev

我想分享在techdev公司开发的项目-trackr-的一些最新的见解。trackr是一个用来跟踪我们的工作时间,创建报告和管理请假的web应用程序。做这个程序的目的有两个,一是作为公司内部使用的实时应用管理程序,另一方面,是为了使用这些新的技术(创新技术堆栈)以便于做一个技术评估。我将描述我们使用的一些技术要点,架构,构建过程和我们碰到的所有的问题或成功实践。

我决定把我的文章分成三个部分来提高可读性。显然这篇文章包含了第一部分,其余的在后面。但首先,让我扫一眼一些基本信息。

Architectural overview

应用程序的基础架构是相当简单的。我们从客户端开始,这是一个基于AngularJS web应用程序。数据来自用Java编写的REST-like后端。这些后端服务允许未来其他应用程序的访问,例如一个Android应用程序。

Other architectural aspects

像一些敏感数据交付和改变服务应该实现一个基于角色的安全性。REST服务应该暴露出来,即资源包含超链接相关资源(cf.HATEOAS)。我们想使用我们的谷歌帐户来访问服务。

Functionality

trackr是一种工具,我们用来跟踪所有的事情如我们的员工的工作时间或假期请求。主管可以从数据中提取出计费时间。在未来,差旅费也将通过trackr管理。

Part 1 – The REST service

第1部分是关于后端服务。虽然一如既往基本上开始作为一个简单的数据库访问层,涉及的技术/框架和日益增长的需求使它更有趣。

Technologies

Java 8才发布不久(当时我们开始着手trackr)我们想尝试构建一个应用程序,来掌握java8的新特性。作为框架,我们选择Spring4。构建应用程序,我们不用旧的Maven ,想试试Gradle

作为容器,我们使用 Tomcat 8 Servlet 3.0环境(我们想避免XML文件),数据库选择PostgreSQL

Gradle

第一个任务是建立整个项目。所以我添加了一个新的Gradle项目在我的IDE中,创建了一个基本的gradle构建文件-build.gradle,它相当于Maven的pom.xml。你会立即注意到Gradle的构建文件如此的小,而且XML样板都消失了。

apply plugin: 'Java'
sourceCompatibility = 1.8
version = '1.0'
repositories {
    mavenCentral()
}
dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
}
task wrapper(type: Wrapper) {
    gradleVersion = '1.10'
}

所以,告诉它我们有一个Java项目与标准src/main/java maven布局我们只是添加apply plugin: 'java'。一个依赖只需要一行(可读性考虑,当然你可以硬塞XML到一行)。因为我们正在构建一个web应用程序我只是需要添加apply plugin: 'war',我将有一个war任务。

从命令行启动Gradle任务时我注意到Gradle的启动时间是慢于Maven的。但这随着项目规模的增大(额外的启动时间常数)并不重要。

IDE支持,至少在我最喜欢的IDE IntelliJ IDEA,不如Maven。自动完成或多或少是不存在的。有时IntelliJ提示警告虽然一切工作正常。我想这就是Groovy插件的问题。

Gradle建议使用Gradle包装器。你必须check in一个小的jar文件在你的源代码中(通常我不喜欢这样,但. .)用于生成UNIX/ Windows环境下的gradlew脚本 。谁pull到代码时如果没有安装Gradle,包装器脚本将是一个替代品,将在后台下载它。我认为这是一个好主意。 

Gradle构建文件是用Groovy编写的。对于那些不了解Groovy,这有时会造成一些困惑。以这个任务为例:

task gruntTest(type: Exec) {
    workingDir 'src/main/webapp/WEB-INF/app'
    executable = 'grunt'
    args = ['test']
}

第2行是一个函数调用另一个变量赋值。有时我不得不使用call来完成一些工作任务。我没有太深入,但对我来说,似乎有点不一致。

但说到任务,可以看到相比一个exec maven插件的配置他们是非常小的。在我们当前的阶段我们的build.gradle文件有155行,包括空行和注释。 

JavaConfig for Spring

以前抱怨Spring就是因为Spring设置应用程序上下文需要一个很大的XML配置文件。Spring 3则不需要这些了,有了一个替代的方法-JavaConfig。此外,当使用一个servlet 3.0标准的web容器时web.xml都是不必要的,所以trackr不包含一个xml文件。我使用了AbstractAnnotationConfigDispatcherServletInitializer(Spring类名总是充满乐趣!)加载我的配置类和启动一个dispatcher servlet。其实我有两个dispatcher servlet,但这并不重要。

JavaConfig好处在于一些Spring项目提供基类配置类,可以通过覆盖方法为你做一些设置。这里是一个例子,一个早期版本的安全配置:

@Configuration
@EnableWebMvcSecurity
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin").password("******").roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/", "/app/bower_components/**").permitAll() //the login page should be able to access CSS and JS files
            .antMatchers("/app/**").authenticated()
            .antMatchers("/api/**").authenticated();

    http.logout().logoutUrl("/logout");

    http.
        formLogin() //this is only for the admin account
            .loginPage("/") //redirect to / if no authenticated session is active
            .loginProcessingUrl("/login/admin") //form has to post to /login/admin
            .defaultSuccessUrl("/app/index.html");
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF > ROLE_EMPLOYEE");
        return roleHierarchy;
    }

    @Bean
    public RoleVoter roleVoter() {
        return new RoleHierarchyVoter(roleHierarchy());
    }
}

正如你所看到的我们扩展基础配置类,覆盖一些方法和定义一些bean。这基本上是在旧的Spring配置文件中的一些特殊的XML标记的一个替代。 

JavaConfig的好处是,你在所有的ide都支持Java代码自动完成提示。使用基于XML的配置你得不到提示或者只有在一些ide中像IDEA Ultimate,Spring Toolsuite才有。

然而JavaConfig有时不如旧学校成熟的XML文件。在上面的例子中配置HttpSecurity只有一个链的方法调用的例子。但这并不编译我们的用例。幸运的是多个语句也能工作。 

对于Spring-Security有一件事是根本不能用JavaConfig配置的——OpenIdAuthenticationFilter。我将说明为什么这是一个问题,我们在本系列后面的部分将说明。

Core Spring

Spring-Security 和Spring-Data-Rest 几乎覆盖了我们所有的需求,我们没必要使用更多的Spring特性。最有趣的部分是完全国际化的要求。这意味着不仅为前端提供翻译(非常简单),而且可以实时切换后端Hibernate的验证消息。我希望我能够在另一篇文章中详细介绍它。

Spring-Data-Rest

Spring-Data使用一个NB的方式来访问你的领域模型在数据库中。Spring-Data-Rest 则更进一步,输出你的repositories作为REST服务。许多事情都是自动完成,而不需要配置或添加东西,例如

  • 资源的GET, POST, PUT, PATCH, DELETE
  • 分页和排序
  • Link到其他资源
  • REST服务的可发现性

Spring-Data-Rest为你生成rest时,自动产生类似这样的rest api:

curl http://localhost:8080/api/
{
  "_links" : {
    "credentials" : {
      "href" : "http://localhost:8080/api/credentials{?page,size,sort}",
      "templated" : true
    },
    "projects" : {
      "href" : "http://localhost:8080/api/projects{?page,size,sort}",
      "templated" : true
    },
    "employees" : {
      "href" : "http://localhost:8080/api/employees{?page,size,sort}",
      "templated" : true
    }
  }
}

curl http://localhost:8080/api/projects/
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/projects/{?page,size,sort}",
      "templated" : true
    },
    "search" : {
      "href" : "http://localhost:8080/api/projects/search"
    }
  },
  "_embedded" : {
    "projects" : [ {
      "id" : 0,
      "version" : 0,
      "identifier" : "1001.1",
      "name" : "Project 1",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/projects/0"
        },
        "company" : {
          "href" : "http://localhost:8080/api/projects/0/company"
        },
        "debitor" : {
          "href" : "http://localhost:8080/api/projects/0/debitor"
        }
      }
    },
    "page" : {
      "size" : 20,
      "totalElements" : 1,
      "totalPages" : 1,
      "number" : 0
    }
  }
}

curl http://localhost:8080/api/projects/search
{
  "_links" : {
    "findByNameLikeOrIdentifierLikeOrderByNameAsc" : {
      "href" : "http://localhost:8080/api/projects/search/findByNameLikeOrIdentifierLikeOrderByNameAsc{?name,identifier}",
      "templated" : true
    },
    "findByIdentifier" : {
      "href" : "http://localhost:8080/api/projects/search/findByIdentifier{?identifier}",
      "templated" : true
    }
  }
}

我在Java项目的领域模型是一个标准的Spring-Data存储库。这使得发布一个REST服务简单至极。

当然有一些可配置性。通过一些注释资源和方法可以发布出来。

但也有缺点。一开始都很容易和快速,但当走到下一个需求,安全,就有一点丑陋。下面是一些常见的安全要求:

  • 员工可以查询他/她自己,而不是其他人的请假申请
  • 主管可以编辑项目但他不允许创建新的。他也不允许改变项目属于的公司
  • 主管可以得到所有假期申请但他们必须不包含自己的请求

现在,Spring-Data-Rest允许所有这些需求的满足,但它更复杂了。对于一些用例Spring-Data-Rest事件处理必须被使用-即,当特定事件发生时,可以呼叫定义好的方法,比如 “employee created”, “employee updated”, “some link of a company changed”.

我认为这样的设计是为了提供一种方法来验证数据或做其他东西,但同样我们可以使用事件进行安全检查。Spring Security一节有更多内容。

另一个缺点是,你失去了一些关注点分离。突然一个能做数据库访问的接口暴露在网络上。也不利于如果你想使一个不同的域模型做为您的REST服务比你的数据库有的。

Spring-Security

还记得,我们有一些安全要求我们的REST服务。并不是每个人都可以做所有的事。我们决定使用一个简单的基于角色的决策系统,即我们有三个角色:管理员、主管和员工。简单的要求是:如果有人不允许做一些通过REST服务返回一个HTTP代码403,不要做任何事情。幸运的是Spring-Data-Rest定义spring web异常处理程序来处理AccessDeniedExceptions,因此我们可以在内部使用spring security。

Spring-Security有几个非常有用的注释。这些注释在GlobalMethodSecurity的上下文中。比如注释方法PreAuthorize使用Spring语言表达式,定义了谁可以访问这个方法。例子:

public interface VacationRequestRepository extends CrudRepository {

    @Override
    @PostAuthorize("hasRole('ROLE_SUPERVISOR') or ( isAuthenticated() and principal.id == returnObject.employee.id )")
    VacationRequest findOne(Long aLong);

    @PreAuthorize("hasRole('ROLE_SUPERVISOR') or ( isAuthenticated() and principal.id == #employee.id )")
    List findByEmployeeOrderByStartDateAsc(@Param("employee") Employee employee);

    @PreAuthorize("hasRole('ROLE_SUPERVISOR')")
    @PostFilter("filterObject.employee.id != principal.id")
    List findByStatusOrderBySubmissionTimeAsc(@Param("status") VacationRequestStatus status);

}

这实际上是相当不言自明的。@PreAuthorize检查方法被调用之前如果满足安全性要求。所以在第二个方法只有主管可以访问这个finder和员工只有他们自己寻找。员工id是保存在登录之后的principal对象。@PostAuthorize帮助如果方法参数没有所有的数据来决定是否一个可以访问的方法。在这种情况下,雇员id需要决定但假期请求id不包含它。最后一个方法是使用一个页面上,主管可以批准或拒绝休假请求,但他们不应该被允许批准自己的请求。所以该方法过滤出来。

当然也有一些缺点。我们的安全要求是耦合的REST服务,即网络层。现在对数据访问层的耦合。只要有一个安全配置应用程序上下文,主要需要访问的方法。这是不利于自动运行作业或内部对资源的访问,在某些情况下当需要执行一个REST调用时,权限必须内部提升。  

测试所有的安全需求是极其重要的。如果有人删除repository注释而没有集成测试将会失败,但降低安全可能是一个巨大的风险。more in 下一节中。  

Testing

每个优秀的程序员都知道,在变化的应用程序中,一个庞大而合理地编写测试套件是很重要的。虽然trackr有一些单元测试和测试如果spring数据存储库的方法做你所想要的不是坏最主要的是测试REST服务。幸运的是,Spring提供了MockMvc测试包。使用MockMvc可以测试一个Spring MVC应用程序以编程方式不依赖浏览器(例如Selenium测试)。它甚至还集成了JsonPath检查响应!

@Test
public void updateAllowedForAdmin() throws Exception {
    Project project = projectDataOnDemand.getRandomObject();
    mockMvc.perform(
        put("/projects/" + project.getId())
            .session(adminSession())
            .content(generateProjectJson(project)))
        .andExpect(status().isOk())
        .andExpect(jsonPath("id", isNotNull()));
}

我使用这种方法测试每个发布的web方法。正如之前说的,如果有人删除Spring-Security PreAuthorize注释,不会有编译错误,但结果可能是致命的。安全需求没有得到满足了。因为没有应用程序的一部分依赖于其他方法调用返回一个HTTP 403代码。测试必须写入以禁止意外删除。

We’ve created a small DSL to pull of the testing in trackr – you can read about the details here

Java 8

为了让Hibernate使用代理模式,需要javassist,只有这一个lib,在java8下兼容不好,升级到新版本解决了这个,一切都很顺利。

这将是一个浪费时间的遍历所有新的Java 8特性在这篇文章中,还有其他的网站做的很好,如Benjamin Winterberg的Java 8 Tutorial 。我使用一些功能和可以告诉你如何使用他们。

The new collections/stream API

我曾经用过Scala和成为一名数学家我不担心的功能,所以进入很容易的事情。我主要使用他们时非常自定义定制响应需求域模型就不代表客户端期望什么(现在我这样做一个实验,客户可以自己和所有数据转换)。

对我来说,stream().collect()和java.util.stream.Collectors联合使用是很棒的东东。

List worktimes = ...;
workTimes.stream().collect(
    groupingBy(
        WorkTime::getEmployee,
        mapping(CustomWorkTime::valueOf, toList())
    )
);

创建一个map,employee 列表作为key,CustomWorkTime列表作为值。很直接,但你需要一些时间来看看Collectors,掌握已有的东西来帮助你理解。

方法引用使用.forEach()是另一个很棒的东东。

IntelliJ有助于发现新功能和使用lambdas。我主要写方法调用,然后插入一个new,按cmd +space来创建所需的接口,实现方法然后转换成lambda。一旦我有一个更好的理解的签名接口lambdas的因为我不需要这样做了。

lambda  

但是你必须小心使用新的StreamAPI。很容易有点过于雄心壮志和编写非常大的表达式,但第二天你要三思。所以我让它保持简单。

java.time.LocalDate

和之前一样我想在每个地方都使用新的日期API,但Hibernate和Spring-Data并不真正支持它。所以我只使用它做更复杂的日期计算。转换很麻烦,说实话我甚至不确定我现在做的所有时间都是对的。API本身我觉得棒极了,基本上就是joda-time。它真的帮助能够调整日期一天而不需要一个日历。

Lambdas

这个是很棒的。比如:

//instead of this
Principal principal = new Principal() {
    @Override
    public String getName() {
        return "admin";
    }
};
//this
Principal principal = () -> "admin";

Yes。

Authorization and Authentication

你可能注意到我们通过Spring-Security-OpenID使用google账户来登录。这意味着,如果您尝试访问服务授权您将迎接一个JSP登录页面。当然不是很合理的一个REST服务,您可能希望访问位置显示一个网页是不可行的。我开始评估如何使用OAuth安全服务,同时保持OpenID登录。这意味着通过OpenID身份验证之后,只有我们的OAuth服务正在运行。这是非常耗时的任务。虽然肯定有趣并不是为我们的第一个client(一个web应用程序)真正需要的,所以我们推迟了。

Go here to read how we’ve added OAuth to trackr

Conclusion

用Java 8和Spring-Data-Rest写后端是有趣和富有建设性的。我认为对我们的用例Spring-Data-Rest符合很好,但是如果你有大量的自定义端点在REST服务或不想放出数据库域模型那就别用它。Spring-Data-Rest之间的交互和spring security需要一些磨合但完全是可能的。Spring测试包真的帮助测试一切以理智的方式。最后,Gradle 是一个不错的改变从Maven但对于这个项目我认为他们非常互相替换的。

想知道我们是怎么使用Spring集成来实现邮件审批的吗? Go here or just continue to part II

 

 

 

 

posted @ 2015-05-02 18:09  文和-Mignet  阅读(465)  评论(0编辑  收藏  举报